diff --git a/CMakeLists.txt b/CMakeLists.txt index 3fc8c2a17..79c0e2209 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,181 +1,181 @@ project(kdepimlibs) # where to look first for cmake modules. This line must be the first one or cmake will use the system's FindFoo.cmake set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules") ############### Build Options ############### option(KDEPIM_ONLY_KLEO "Only build the libraries needed by Kleopatra." FALSE) ############### The kdepimlibs version (used e.g. in KdepimLibsConfig.cmake) ############### set(KDEPIMLIBS_VERSION_MAJOR 4) set(KDEPIMLIBS_VERSION_MINOR 3) set(KDEPIMLIBS_VERSION_PATCH 60) set(KDEPIMLIBS_VERSION ${KDEPIMLIBS_VERSION_MAJOR}.${KDEPIMLIBS_VERSION_MINOR}.${KDEPIMLIBS_VERSION_PATCH}) ############### search packages used by KDE ############### find_package(KDE4 4.2.90 REQUIRED) include(KDE4Defaults) include(MacroLibrary) ############### Needed commands before building anything ############### add_definitions (${QT_DEFINITIONS} ${KDE4_DEFINITIONS}) include_directories (${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR} ${KDE4_INCLUDES}) ############### Find the stuff we need ############### set(Boost_MINIMUM_VERSION "1.33.1") find_package(Boost) -macro_log_feature(Boost_FOUND "boost" "Boost C++ Libraries" "http://www.boost.org" TRUE "" "Required by several critical KDEPIM apps.") +macro_log_feature(Boost_FOUND "Boost" "Boost C++ Libraries" "http://www.boost.org" TRUE "" "Required by several critical KDEPIM apps.") #FindGpgme.cmake already handles the log message but we must ensure it is required. find_package(Gpgme REQUIRED) # configure macros if (GPGME_FOUND) include (gpgme++/ConfigureChecks.cmake) endif (GPGME_FOUND) if (NOT KDEPIM_ONLY_KLEO) #FindAkonadi.cmake is only there for compatibility reasons, but we don't want to use that. find_package(Akonadi 1.1.91 QUIET NO_MODULE) - macro_log_feature(Akonadi_FOUND "Akonadi" "Akonadi server (from kdesupport)" "http://pim.kde.org/akonadi" TRUE "1.1.95" "Akonadi is required to build KdepimLibs.") + macro_log_feature(Akonadi_FOUND "Akonadi" "Akonadi server libraries (from kdesupport)" "http://pim.kde.org/akonadi" TRUE "1.1.95" "Akonadi is required to build KdepimLibs.") find_package(Sasl2) macro_log_feature(SASL2_FOUND "cyrus-sasl" "Cyrus SASL API" "http://asg.web.cmu.edu/sasl/sasl-library.html" TRUE "" "Required to support authentication of logins in the IMAP and Sieve kioslaves.") include (ConfigureChecks.cmake) set(SHARED_MIME_INFO_MINIMUM_VERSION "0.30") find_package(SharedMimeInfo) macro_log_feature(SHARED_MIME_INFO_FOUND "SMI" "SharedMimeInfo" "http://freedesktop.org/wiki/Software/shared-mime-info" TRUE "0.30" "SharedMimeInfo is required.") endif (NOT KDEPIM_ONLY_KLEO) ############### Now, we add the KDEPIMLibs components ############### # These targets will always be built add_subdirectory(cmake) add_subdirectory(gpgme++) add_subdirectory(qgpgme) if (NOT KDEPIM_ONLY_KLEO) add_subdirectory(akonadi) add_subdirectory(kabc) add_subdirectory(kblog) add_subdirectory(kcal) add_subdirectory(kholidays) add_subdirectory(kimap) add_subdirectory(kioslave) add_subdirectory(kldap) add_subdirectory(kmime) add_subdirectory(kpimidentities) add_subdirectory(kpimutils) add_subdirectory(kpimtextedit) add_subdirectory(kresources) add_subdirectory(ktnef) add_subdirectory(kxmlrpcclient) add_subdirectory(mailtransport) add_subdirectory(microblog) add_subdirectory(outboxinterface) add_subdirectory(syndication) # Build the CamelCase headers add_subdirectory(includes) endif (NOT KDEPIM_ONLY_KLEO) # doc must be a subdir of kdepimlibs macro_optional_add_subdirectory(doc) # All done, let's display what we found... macro_display_feature_log() ############### Here we install some extra stuff ############### if (NOT KDEPIM_ONLY_KLEO) install(FILES kdepimlibs-mime.xml DESTINATION ${XDG_MIME_INSTALL_DIR}) update_xdg_mimetypes(${XDG_MIME_INSTALL_DIR}) endif (NOT KDEPIM_ONLY_KLEO) # now create the KdepimLibsConfig.cmake file, which will be loaded by # kdelibs/cmake/modules/FindKdepimLibs.cmake and which has to contain all information # about the installed kdepimlibs anybody would like to have. Alex # we need the absolute directories where stuff will be installed too # but since the variables which contain the destinations can be relative # or absolute paths, we need this macro to make them all absoulte, Alex macro(MAKE_INSTALL_PATH_ABSOLUTE out in) if (IS_ABSOLUTE "${in}") # IS_ABSOLUTE is new since cmake 2.4.8 set(${out} "${in}") else (IS_ABSOLUTE "${in}") set(${out} "\${KDEPIMLIBS_INSTALL_DIR}/${in}") endif (IS_ABSOLUTE "${in}") endmacro(MAKE_INSTALL_PATH_ABSOLUTE out in) # all the following variables are put into KdepimLibsConfig.cmake, so # they are usable by projects using kdepimlibs. Alex make_install_path_absolute(KDEPIMLIBS_DATA_DIR ${DATA_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_DBUS_INTERFACES_DIR ${DBUS_INTERFACES_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_DBUS_SERVICES_DIR ${DBUS_SERVICES_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_INCLUDE_DIR ${INCLUDE_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_LIB_DIR ${LIB_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_BIN_DIR ${BIN_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_LIBEXEC_DIR ${LIBEXEC_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_SBIN_DIR ${SBIN_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_HTML_DIR ${HTML_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_CONFIG_DIR ${CONFIG_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_ICON_DIR ${ICON_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_KCFG_DIR ${KCFG_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_LOCALE_DIR ${LOCALE_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_MIME_DIR ${MIME_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_SOUND_DIR ${SOUND_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_TEMPLATES_DIR ${TEMPLATES_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_WALLPAPER_DIR ${WALLPAPER_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_KCONF_UPDATE_DIR ${KCONF_UPDATE_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_AUTOSTART_DIR ${AUTOSTART_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_XDG_APPS_DIR ${XDG_APPS_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_XDG_DIRECTORY_DIR ${XDG_DIRECTORY_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_SYSCONF_DIR ${SYSCONF_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_MAN_DIR ${MAN_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_INFO_DIR ${INFO_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_SERVICES_DIR ${SERVICES_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_SERVICETYPES_DIR ${SERVICETYPES_INSTALL_DIR}) # Used in configure_file() and install(EXPORT) set(KDEPIMLIBS_TARGET_PREFIX KDEPIMLibs__) # this file is installed and contains all necessary information about the installed kdepimlibs, it also loads the file with the exported targets configure_file(KdepimLibsConfig.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/KdepimLibsConfig.cmake" @ONLY) # this file will be installed too and will be used by cmake when searching for the Config.cmake file to check the version of kdepimlibs, Alex macro_write_basic_cmake_version_file(${CMAKE_CURRENT_BINARY_DIR}/KdepimLibsConfigVersion.cmake ${KDEPIMLIBS_VERSION_MAJOR} ${KDEPIMLIBS_VERSION_MINOR} ${KDEPIMLIBS_VERSION_PATCH}) set(_KdepimLibsConfig_INSTALL_DIR ${LIB_INSTALL_DIR}/KdepimLibs/cmake) # places where find_package() looks for FooConfig.cmake files: # CMake >= 2.6.0 looks in lib/Foo*/cmake/, CMake >= 2.6.3 also looks in # lib/cmake/Foo*/, which packagers prefer. So they can set the KDE4_USE_COMMON_CMAKE_PACKAGE_CONFIG_DIR # option to have kdepimlibs install its Config file there. Alex if(KDE4_USE_COMMON_CMAKE_PACKAGE_CONFIG_DIR) set(_KdepimLibsConfig_INSTALL_DIR ${LIB_INSTALL_DIR}/cmake/KdepimLibs) endif(KDE4_USE_COMMON_CMAKE_PACKAGE_CONFIG_DIR) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/KdepimLibsConfigVersion.cmake ${CMAKE_CURRENT_BINARY_DIR}/KdepimLibsConfig.cmake DESTINATION ${_KdepimLibsConfig_INSTALL_DIR} ) # Install the file with the exported targets, use ${KDEPIMLIBS_TARGET_PREFIX} as prefix for the names of these targets, Alex install(EXPORT kdepimlibsLibraryTargets NAMESPACE ${KDEPIMLIBS_TARGET_PREFIX} DESTINATION ${_KdepimLibsConfig_INSTALL_DIR} FILE KDEPimLibsLibraryTargetsWithPrefix.cmake ) # Install a KDEPimLibsDependencies.cmake so people using kdepimlibs 4.2 with kdelibs < 4.2 get a useful error message, Alex file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/KDEPimLibsDependencies.cmake "\n message(FATAL_ERROR \"For using this version of kdepimlibs (${KDEPIMLIBS_VERSION}) you need a newer version of kdelibs, please update.\")\n") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/KDEPimLibsDependencies.cmake DESTINATION ${DATA_INSTALL_DIR}/cmake/modules) diff --git a/akonadi/control.cpp b/akonadi/control.cpp index 38463edc9..42d20582d 100644 --- a/akonadi/control.cpp +++ b/akonadi/control.cpp @@ -1,249 +1,258 @@ /* Copyright (c) 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "control.h" #include "servermanager.h" #include "ui_controlprogressindicator.h" #include "selftestdialog_p.h" #include "erroroverlay_p.h" #include "firstrun_p.h" #include #include #include #include +#include #include #include using namespace Akonadi; class ControlProgressIndicator : public QFrame { public: ControlProgressIndicator( QWidget *parent = 0 ) : QFrame( parent ) { setWindowModality( Qt::ApplicationModal ); resize( 400, 100 ); setWindowFlags( Qt::FramelessWindowHint | Qt::Dialog ); ui.setupUi( this ); setFrameShadow( QFrame::Plain ); setFrameShape( QFrame::Box ); } void setMessage( const QString &msg ) { ui.statusLabel->setText( msg ); } Ui::ControlProgressIndicator ui; }; /** * @internal */ class Control::Private { public: Private( Control *parent ) : mParent( parent ), mEventLoop( 0 ), mProgressIndicator( 0 ), mFirstRunner( 0 ), mSuccess( false ), mStarting( false ), mStopping( false ) { KGlobal::locale()->insertCatalog( QString::fromLatin1("libakonadi") ); if ( ServerManager::isRunning() ) mFirstRunner = new Firstrun( mParent ); } ~Private() { delete mProgressIndicator; } void setupProgressIndicator( const QString &msg, QWidget *parent = 0 ) { if ( mProgressIndicator ) return; mProgressIndicator = new ControlProgressIndicator( parent ); mProgressIndicator->setMessage( msg ); } void createErrorOverlays() { foreach ( QWidget* widget, mPendingOverlays ) new ErrorOverlay( widget ); mPendingOverlays.clear(); } bool exec(); void serverStarted(); void serverStopped(); QPointer mParent; QEventLoop *mEventLoop; QPointer mProgressIndicator; QList mPendingOverlays; Firstrun *mFirstRunner; bool mSuccess; bool mStarting; bool mStopping; }; class StaticControl : public Control { public: StaticControl() : Control() {} }; K_GLOBAL_STATIC( StaticControl, s_instance ) +void Control::cleanup() +{ + s_instance.destroy(); +} bool Control::Private::exec() { if ( mProgressIndicator ) mProgressIndicator->show(); kDebug( 5250 ) << "Starting Akonadi (using an event loop)."; mEventLoop = new QEventLoop( mParent ); // safety timeout QTimer::singleShot( 10000, mEventLoop, SLOT(quit()) ); mEventLoop->exec(); mEventLoop->deleteLater(); mEventLoop = 0; if ( !mSuccess ) { kWarning( 5250 ) << "Could not start/stop Akonadi!"; if ( mProgressIndicator && mStarting ) { QPointer dlg = new SelfTestDialog( mProgressIndicator->parentWidget() ); dlg->exec(); delete dlg; if ( !mParent ) return false; } } delete mProgressIndicator; mProgressIndicator = 0; mStarting = false; mStopping = false; const bool rv = mSuccess; mSuccess = false; return rv; } void Control::Private::serverStarted() { if ( mEventLoop && mEventLoop->isRunning() && mStarting ) { mEventLoop->quit(); mSuccess = true; } if ( !mFirstRunner ) mFirstRunner = new Firstrun( mParent ); } void Control::Private::serverStopped() { if ( mEventLoop && mEventLoop->isRunning() && mStopping ) { mEventLoop->quit(); mSuccess = true; } } Control::Control() : d( new Private( this ) ) { connect( ServerManager::self(), SIGNAL(started()), SLOT(serverStarted()) ); connect( ServerManager::self(), SIGNAL(stopped()), SLOT(serverStopped()) ); + // mProgressIndicator is a widget, so it better be deleted before the QApplication is deleted + // Otherwise we get a crash in QCursor code with Qt-4.5 + if ( QCoreApplication::instance() ) + connect( QCoreApplication::instance(), SIGNAL(aboutToQuit()), this, SLOT(cleanup()) ); } Control::~Control() { delete d; } bool Control::start() { if ( s_instance->d->mStopping ) return false; if ( ServerManager::isRunning() || s_instance->d->mEventLoop ) return true; s_instance->d->mStarting = true; if ( !ServerManager::start() ) return false; return s_instance->d->exec(); } bool Control::stop() { if ( s_instance->d->mStarting ) return false; if ( !ServerManager::isRunning() || s_instance->d->mEventLoop ) return true; s_instance->d->mStopping = true; if ( !ServerManager::stop() ) return false; return s_instance->d->exec(); } bool Control::restart() { if ( ServerManager::isRunning() ) { if ( !stop() ) return false; } return start(); } bool Control::start(QWidget * parent) { s_instance->d->setupProgressIndicator( i18n( "Starting Akonadi server..." ), parent ); return start(); } bool Control::stop(QWidget * parent) { s_instance->d->setupProgressIndicator( i18n( "Stopping Akonadi server..." ), parent ); return stop(); } bool Control::restart(QWidget * parent) { if ( ServerManager::isRunning() ) { if ( !stop( parent ) ) return false; } return start( parent ); } void Control::widgetNeedsAkonadi(QWidget * widget) { s_instance->d->mPendingOverlays.append( widget ); // delay the overlay creation since we rely on widget being reparented // correctly already QTimer::singleShot( 0, s_instance, SLOT(createErrorOverlays()) ); } #include "control.moc" diff --git a/akonadi/control.h b/akonadi/control.h index 97d663fb7..4e1ce9dc2 100644 --- a/akonadi/control.h +++ b/akonadi/control.h @@ -1,142 +1,145 @@ /* Copyright (c) 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_CONTROL_H #define AKONADI_CONTROL_H #include "akonadi_export.h" #include namespace Akonadi { /** * @short Provides methods to control the Akonadi server process. * * This class provides high-level methods to control the Akonadi * server. These methods are synchronously (ie. use a sub-eventloop) * and can show dialogs. For more low-level methods see * Akonadi::ServerManager. * * While the Akonadi server normally is started by the KDE session * manager, it is not guaranteed that your application is running * inside a KDE session. Therefore it is recommended to execute * Akonadi::Control::start() during startup to ensure the Akonadi * server is running. * * Example: * * @code * * if ( !Akonadi::Control::start() ) { * qDebug() << "Unable to start Akonadi server, exit application"; * return 1; * } else { * ... * } * * @endcode * * @author Volker Krause * * @see Akonadi::ServerManager */ class AKONADI_EXPORT Control : public QObject { Q_OBJECT public: /** * Destroys the control object. */ ~Control(); /** * Starts the Akonadi server synchronously if it is not already running. * @return @c true if the server was started successfully or was already * running, @c false otherwise */ static bool start(); /** * Same as start(), but with GUI feedback. * @param parent The parent widget. * @since 4.2 */ static bool start( QWidget *parent ); /** * Stops the Akonadi server synchronously if it is currently running. * @return @c true if the server was shutdown successfully or was * not running at all, @c false otherwise. * @since 4.2 */ static bool stop(); /** * Same as stop(), but with GUI feedback. * @param parent The parent widget. * @since 4.2 */ static bool stop( QWidget *parent ); /** * Restarts the Akonadi server synchronously. * @return @c true if the restart was successful, @c false otherwise, * the server state is undefined in this case. * @since 4.2 */ static bool restart(); /** * Same as restart(), but with GUI feedback. * @param parent The parent widget. * @since 4.2 */ static bool restart( QWidget *parent ); /** * Disable the given widget when Akonadi is not operational and show * an error overlay (given enough space). Cascading use is automatically * detected. * @param widget The widget depending on Akonadi being operational. * @since 4.2 */ static void widgetNeedsAkonadi( QWidget *widget ); protected: /** * Creates the control object. */ Control(); + private Q_SLOTS: + void cleanup(); + private: //@cond PRIVATE class Private; Private* const d; Q_PRIVATE_SLOT( d, void serverStarted() ) Q_PRIVATE_SLOT( d, void serverStopped() ) Q_PRIVATE_SLOT( d, void createErrorOverlays() ) //@endcond }; } #endif diff --git a/akonadi/kmime/CMakeLists.txt b/akonadi/kmime/CMakeLists.txt index fa9b634b8..f5765ea12 100644 --- a/akonadi/kmime/CMakeLists.txt +++ b/akonadi/kmime/CMakeLists.txt @@ -1,44 +1,44 @@ include_directories( - ${CMAKE_SOURCE_DIR}/ - ${QT_QTDBUS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/ + ${QT_QTDBUS_INCLUDE_DIR} ${Boost_INCLUDE_DIR} ) add_subdirectory( tests ) set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_CAST_FROM_ASCII -DQT_NO_CAST_TO_ASCII ${KDE4_ENABLE_EXCEPTIONS}" ) ########### next target ############### set( kmimeakonadi_LIB_SRC addressattribute.cpp localfolders.cpp localfoldersbuildjob.cpp messagemodel.cpp messageparts.cpp messagethreadingattribute.cpp messagethreaderproxymodel.cpp resourcesynchronizationjob.cpp # copied from playground/pim/akonaditest/resourcetester ) kde4_add_kcfg_files( kmimeakonadi_LIB_SRC localfolderssettings.kcfgc ) install( FILES localfolders.kcfg DESTINATION ${KCFG_INSTALL_DIR} ) kde4_add_library( akonadi-kmime SHARED ${kmimeakonadi_LIB_SRC} ) -target_link_libraries( akonadi-kmime akonadi-kde kmime ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} ${KDE4_KIO_LIBS} ) +target_link_libraries( akonadi-kmime akonadi-kde kmime ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} ${KDE4_KDEUI_LIBS} ) set_target_properties( akonadi-kmime PROPERTIES VERSION ${GENERIC_LIB_VERSION} SOVERSION ${GENERIC_LIB_SOVERSION} ) install(TARGETS akonadi-kmime EXPORT kdepimlibsLibraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS}) ########### install files ############### install( FILES addressattribute.h localfolders.h akonadi-kmime_export.h messagemodel.h messageparts.h messagethreadingattribute.h messagethreaderproxymodel.h DESTINATION ${INCLUDE_INSTALL_DIR}/akonadi/kmime COMPONENT Devel ) diff --git a/akonadi/kmime/messagemodel.cpp b/akonadi/kmime/messagemodel.cpp index e0387287c..039345016 100644 --- a/akonadi/kmime/messagemodel.cpp +++ b/akonadi/kmime/messagemodel.cpp @@ -1,170 +1,170 @@ /* Copyright (c) 2006 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "messagemodel.h" #include "messageparts.h" #include #include #include #include #include typedef boost::shared_ptr MessagePtr; #include #include #include #include #include using namespace Akonadi; class Akonadi::MessageModel::Private { public: }; MessageModel::MessageModel( QObject *parent ) : ItemModel( parent ), d( new Private() ) { fetchScope().fetchPayloadPart( MessagePart::Envelope ); } MessageModel::~MessageModel( ) { delete d; } QStringList MessageModel::mimeTypes() const { return QStringList() << QLatin1String("text/uri-list") << QLatin1String("message/rfc822"); } int MessageModel::rowCount( const QModelIndex & parent ) const { if ( collection().isValid() && !collection().contentMimeTypes().contains( QLatin1String("message/rfc822") ) && collection().contentMimeTypes() != QStringList( QLatin1String("inode/directory") ) ) return 1; return ItemModel::rowCount(); } int MessageModel::columnCount( const QModelIndex & parent ) const { if ( collection().isValid() && !collection().contentMimeTypes().contains( QLatin1String("message/rfc822") ) && collection().contentMimeTypes() != QStringList( QLatin1String("inode/directory") ) ) return 1; if ( !parent.isValid() ) return 5; // keep in sync with the column type enum return 0; } QVariant MessageModel::data( const QModelIndex & index, int role ) const { if ( !index.isValid() ) return QVariant(); if ( index.row() >= rowCount() ) return QVariant(); if ( !collection().contentMimeTypes().contains( QLatin1String("message/rfc822") ) ) { if ( role == Qt::DisplayRole ) return i18n( "This model can only handle email folders. The current collection holds mimetypes: %1", collection().contentMimeTypes().join( QLatin1String(",") ) ); else return QVariant(); } Item item = itemForIndex( index ); if ( !item.hasPayload() ) return QVariant(); MessagePtr msg = item.payload(); if ( role == Qt::DisplayRole ) { switch ( index.column() ) { case Subject: return msg->subject()->asUnicodeString(); case Sender: return msg->from()->asUnicodeString(); case Receiver: return msg->to()->asUnicodeString(); case Date: return KGlobal::locale()->formatDateTime( msg->date()->dateTime().toLocalZone(), KLocale::FancyLongDate ); case Size: if ( item.size() == 0 ) return i18nc( "No size available", "-" ); else - return KIO::convertSize( KIO::filesize_t( item.size() ) ); + return KGlobal::locale()->formatByteSize( item.size() ); default: return QVariant(); } } else if ( role == Qt::EditRole ) { switch ( index.column() ) { case Subject: return msg->subject()->asUnicodeString(); case Sender: return msg->from()->asUnicodeString(); case Receiver: return msg->to()->asUnicodeString(); case Date: return msg->date()->dateTime().dateTime(); case Size: return item.size(); default: return QVariant(); } } return ItemModel::data( index, role ); } QVariant MessageModel::headerData( int section, Qt::Orientation orientation, int role ) const { if ( collection().isValid() && !collection().contentMimeTypes().contains( QLatin1String("message/rfc822") ) && collection().contentMimeTypes() != QStringList( QLatin1String("inode/directory") ) ) return QVariant(); if ( orientation == Qt::Horizontal && role == Qt::DisplayRole ) { switch ( section ) { case Subject: return i18nc( "@title:column, message (e.g. email) subject", "Subject" ); case Sender: return i18nc( "@title:column, sender of message (e.g. email)", "Sender" ); case Receiver: return i18nc( "@title:column, receiver of message (e.g. email)", "Receiver" ); case Date: return i18nc( "@title:column, message (e.g. email) timestamp", "Date" ); case Size: return i18nc( "@title:column, message (e.g. email) size", "Size" ); default: return QString(); } } return ItemModel::headerData( section, orientation, role ); } #include "messagemodel.moc" diff --git a/akonadi/tests/CMakeLists.txt b/akonadi/tests/CMakeLists.txt index 2fbe42172..748128916 100644 --- a/akonadi/tests/CMakeLists.txt +++ b/akonadi/tests/CMakeLists.txt @@ -1,124 +1,126 @@ if(${EXECUTABLE_OUTPUT_PATH}) set( PREVIOUS_EXEC_OUTPUT_PATH ${EXECUTABLE_OUTPUT_PATH} ) else(${EXECUTABLE_OUTPUT_PATH}) set( PREVIOUS_EXEC_OUTPUT_PATH . ) endif(${EXECUTABLE_OUTPUT_PATH}) set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${KDE4_ENABLE_EXCEPTIONS}" ) include_directories( ${CMAKE_SOURCE_DIR}/akonadi ${CMAKE_CURRENT_SOURCE_DIR}/../ ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}/../ ${Boost_INCLUDE_DIR} ${AKONADI_INCLUDE_DIR} ) # add testrunner (application for managing a self-contained test # environment) add_subdirectory(testrunner) # add benchmarker add_subdirectory(benchmarker) # convenience macro to add akonadi demo application macro(add_akonadi_demo _source) set(_test ${_source}) get_filename_component(_name ${_source} NAME_WE) kde4_add_executable(${_name} TEST ${_test}) target_link_libraries(${_name} akonadi-kde akonadi-kmime ${QT_QTCORE_LIBRARY} ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} ${KDE4_KDEUI_LIBS}) endmacro(add_akonadi_demo) # convenience macro to add akonadi qtestlib unit-tests macro(add_akonadi_test _source) set(_test ${_source}) get_filename_component(_name ${_source} NAME_WE) kde4_add_unit_test(${_name} TESTNAME akonadi-${_name} ${_test}) target_link_libraries(${_name} akonadi-kde akonadi-kmime ${QT_QTTEST_LIBRARY} ${QT_QTCORE_LIBRARY} ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} ${AKONADI_COMMON_LIBRARIES}) endmacro(add_akonadi_test) # convenience macro to add akonadi testrunner unit-tests macro(add_akonadi_isolated_test _source) set(_test ${_source}) get_filename_component(_name ${_source} NAME_WE) kde4_add_executable(${_name} TEST ${_test}) target_link_libraries(${_name} akonadi-kde akonadi-kmime ${QT_QTTEST_LIBRARY} ${QT_QTCORE_LIBRARY} ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} ${AKONADI_COMMON_LIBRARIES}) # based on kde4_add_unit_test if (WIN32) get_target_property( _loc ${_name} LOCATION ) set(_executable ${_loc}.bat) set(_testrunner ${PREVIOUS_EXEC_OUTPUT_PATH}/akonaditest.bat) else (WIN32) set(_executable ${EXECUTABLE_OUTPUT_PATH}/${_name}) set(_testrunner ${PREVIOUS_EXEC_OUTPUT_PATH}/akonaditest) endif (WIN32) if (UNIX) set(_executable ${_executable}.shell) set(_testrunner ${_testrunner}.shell) endif (UNIX) - add_test( akonadi-db-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-db.xml ${_executable} ) - add_test( akonadi-fs-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-fs.xml ${_executable} ) + add_test( akonadi-mysql-db-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-mysql-db.xml ${_executable} ) + add_test( akonadi-mysql-fs-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-mysql-fs.xml ${_executable} ) + #add_test( akonadi-postgresql-fs-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-postgresql-fs.xml ${_executable} ) + #add_test( akonadi-postgresql-fs-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-postgresql-fs.xml ${_executable} ) #add_test( akonadi-sqlite-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-sqlite.xml ${_executable} ) endmacro(add_akonadi_isolated_test) # demo applications add_akonadi_demo(itemdumper.cpp) add_akonadi_demo(subscriber.cpp) add_akonadi_demo(headfetcher.cpp) add_akonadi_demo(agentinstancewidgettest.cpp) add_akonadi_demo(agenttypewidgettest.cpp) add_akonadi_demo(pluginloadertest.cpp) add_akonadi_demo(selftester.cpp) kde4_add_executable( akonadi-firstrun TEST ../firstrun.cpp firstrunner.cpp ) target_link_libraries( akonadi-firstrun akonadi-kde ${KDE4_KDEUI_LIBS} ) # qtestlib unit tests add_akonadi_test(imapparsertest.cpp) add_akonadi_test(imapsettest.cpp) add_akonadi_test(itemhydratest.cpp) add_akonadi_test(itemtest.cpp) add_akonadi_test(itemserializertest.cpp) add_akonadi_test(mimetypecheckertest.cpp) add_akonadi_test(protocolhelpertest.cpp) # qtestlib tests that need non-exported stuff from akonadi-kde kde4_add_unit_test(resourceschedulertest TESTNAME akonadi-resourceschedulertest resourceschedulertest.cpp ../resourcescheduler.cpp) target_link_libraries(resourceschedulertest akonadi-kde ${QT_QTTEST_LIBRARY} ${QT_QTCORE_LIBRARY} ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} ${AKONADI_COMMON_LIBRARIES}) # testrunner tests add_akonadi_isolated_test(testenvironmenttest.cpp) add_akonadi_isolated_test(autoincrementtest.cpp) add_akonadi_isolated_test(attributefactorytest.cpp) add_akonadi_isolated_test(collectionjobtest.cpp) add_akonadi_isolated_test(collectionpathresolvertest.cpp) add_akonadi_isolated_test(collectionattributetest.cpp) add_akonadi_isolated_test(itemfetchtest.cpp) add_akonadi_isolated_test(itemappendtest.cpp) add_akonadi_isolated_test(itemstoretest.cpp) add_akonadi_isolated_test(itemdeletetest.cpp) add_akonadi_isolated_test(monitortest.cpp) add_akonadi_isolated_test(searchjobtest.cpp) add_akonadi_isolated_test(changerecordertest.cpp) add_akonadi_isolated_test(resourcetest.cpp) add_akonadi_isolated_test(subscriptiontest.cpp) add_akonadi_isolated_test(transactiontest.cpp) add_akonadi_isolated_test(filteractiontest.cpp) add_akonadi_isolated_test(itemcopytest.cpp) add_akonadi_isolated_test(itemmovetest.cpp) add_akonadi_isolated_test(collectioncopytest.cpp) add_akonadi_isolated_test(collectionmovetest.cpp) add_akonadi_isolated_test(collectionsynctest.cpp) add_akonadi_isolated_test(itemsynctest.cpp) add_akonadi_isolated_test(linktest.cpp) add_akonadi_isolated_test(cachetest.cpp) add_akonadi_isolated_test(servermanagertest.cpp) add_akonadi_isolated_test(collectioncreator.cpp) add_akonadi_isolated_test(itembenchmark.cpp) diff --git a/akonadi/tests/unittestenv/config-db.xml b/akonadi/tests/unittestenv/config-mysql-db.xml similarity index 84% copy from akonadi/tests/unittestenv/config-db.xml copy to akonadi/tests/unittestenv/config-mysql-db.xml index 546171926..a9aeca7c9 100644 --- a/akonadi/tests/unittestenv/config-db.xml +++ b/akonadi/tests/unittestenv/config-mysql-db.xml @@ -1,8 +1,8 @@ kdehome - xdgconfig.db + xdgconfig-mysql.db xdglocal akonadi_knut_resource akonadi_knut_resource akonadi_knut_resource diff --git a/akonadi/tests/unittestenv/config-fs.xml b/akonadi/tests/unittestenv/config-mysql-fs.xml similarity index 84% rename from akonadi/tests/unittestenv/config-fs.xml rename to akonadi/tests/unittestenv/config-mysql-fs.xml index ee81d2ee4..75745aae5 100644 --- a/akonadi/tests/unittestenv/config-fs.xml +++ b/akonadi/tests/unittestenv/config-mysql-fs.xml @@ -1,8 +1,8 @@ kdehome - xdgconfig.fs + xdgconfig-mysql.fs xdglocal akonadi_knut_resource akonadi_knut_resource akonadi_knut_resource diff --git a/akonadi/tests/unittestenv/config-db.xml b/akonadi/tests/unittestenv/config-postgresql-db.xml similarity index 83% copy from akonadi/tests/unittestenv/config-db.xml copy to akonadi/tests/unittestenv/config-postgresql-db.xml index 546171926..4c1e7b2cc 100644 --- a/akonadi/tests/unittestenv/config-db.xml +++ b/akonadi/tests/unittestenv/config-postgresql-db.xml @@ -1,8 +1,8 @@ kdehome - xdgconfig.db + xdgconfig-postgresql.db xdglocal akonadi_knut_resource akonadi_knut_resource akonadi_knut_resource diff --git a/akonadi/tests/unittestenv/config-db.xml b/akonadi/tests/unittestenv/config-postgresql-fs.xml similarity index 83% rename from akonadi/tests/unittestenv/config-db.xml rename to akonadi/tests/unittestenv/config-postgresql-fs.xml index 546171926..279a4836a 100644 --- a/akonadi/tests/unittestenv/config-db.xml +++ b/akonadi/tests/unittestenv/config-postgresql-fs.xml @@ -1,8 +1,8 @@ kdehome - xdgconfig.db + xdgconfig-postgresql.fs xdglocal akonadi_knut_resource akonadi_knut_resource akonadi_knut_resource diff --git a/akonadi/tests/unittestenv/xdgconfig.db/akonadi/akonadiserverrc b/akonadi/tests/unittestenv/xdgconfig-mysql.db/akonadi/akonadiserverrc similarity index 100% rename from akonadi/tests/unittestenv/xdgconfig.db/akonadi/akonadiserverrc rename to akonadi/tests/unittestenv/xdgconfig-mysql.db/akonadi/akonadiserverrc diff --git a/akonadi/tests/unittestenv/xdgconfig.fs/akonadi/akonadiserverrc b/akonadi/tests/unittestenv/xdgconfig-mysql.fs/akonadi/akonadiserverrc similarity index 100% rename from akonadi/tests/unittestenv/xdgconfig.fs/akonadi/akonadiserverrc rename to akonadi/tests/unittestenv/xdgconfig-mysql.fs/akonadi/akonadiserverrc diff --git a/akonadi/tests/unittestenv/xdgconfig-postgresql.db/akonadi/akonadiserverrc b/akonadi/tests/unittestenv/xdgconfig-postgresql.db/akonadi/akonadiserverrc new file mode 100644 index 000000000..0bd4ae1a4 --- /dev/null +++ b/akonadi/tests/unittestenv/xdgconfig-postgresql.db/akonadi/akonadiserverrc @@ -0,0 +1,15 @@ +[%General] +Driver=QPSQL +ExternalPayload=false + +[Search] +Manager=Dummy + +[QPSQL] +Name=post_table +User=user +Password=pass +Options= +StartServer=false +Host= +Port=5432 diff --git a/akonadi/tests/unittestenv/xdgconfig-postgresql.fs/akonadi/akonadiserverrc b/akonadi/tests/unittestenv/xdgconfig-postgresql.fs/akonadi/akonadiserverrc new file mode 100644 index 000000000..b14bb39c6 --- /dev/null +++ b/akonadi/tests/unittestenv/xdgconfig-postgresql.fs/akonadi/akonadiserverrc @@ -0,0 +1,17 @@ +[%General] +Driver=QPSQL +SizeThreshold=0 +ExternalPayload=true + +[Search] +Manager=Dummy + +[QPSQL] +Name=post_table +User=user +Password=pass +Options= +StartServer=false +Host= +Port=5432 + diff --git a/cmake/modules/FindGpgme.cmake b/cmake/modules/FindGpgme.cmake index 4145a0501..27730bd2d 100644 --- a/cmake/modules/FindGpgme.cmake +++ b/cmake/modules/FindGpgme.cmake @@ -1,393 +1,393 @@ # - Try to find the gpgme library # # Algorithm: # - Windows: # On Windows, there's three gpgme variants: gpgme{,-glib,-qt}. # - The variant used determines the event loop integration possible: # - gpgme: no event loop integration possible, only synchronous operations supported # - gpgme-glib: glib event loop integration possible, only asynchronous operations supported # - gpgme-qt: qt event loop integration possible, only asynchronous operations supported # - GPGME_{VANILLA,GLIB,QT}_{FOUND,LIBRARIES} will be set for each of the above # - GPGME_INCLUDES is the same for all of the above # - GPGME_FOUND is set if any of the above was found # - *nix: # There's also three variants: gpgme{,-pthread,-pth}. # - The variant used determines the multithreaded use possible: # - gpgme: no multithreading support available # - gpgme-pthread: multithreading available using POSIX threads # - gpgme-pth: multithreading available using GNU PTH (cooperative multithreading) # - GPGME_{VANILLA,PTH,PTHREAD}_{FOUND,LIBRARIES} will be set for each of the above # - GPGME_INCLUDES is the same for all of the above # - GPGME_FOUND is set if any of the above was found # # GPGME_LIBRARY_DIR - the directory where the libraries are located # # THIS IS ALMOST A 1:1 COPY OF FindAssuan.cmake in kdepim. # Any changes here likely apply there, too. # # do away with crappy condition repetition on else/endfoo set( CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS_gpgme_saved ${CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS} ) set( CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS true ) #if this is built-in, please replace, if it isn't, export into a MacroToBool.cmake of it's own macro( macro_bool_to_bool FOUND_VAR ) foreach( _current_VAR ${ARGN} ) if ( ${FOUND_VAR} ) set( ${_current_VAR} TRUE ) else() set( ${_current_VAR} FALSE ) endif() endforeach() endmacro() include (MacroEnsureVersion) if ( WIN32 ) # On Windows, we don't have a gpgme-config script, so we need to # look for the stuff ourselves: # in cmake, AND and OR have the same precedence, there's no # subexpressions, and expressions are evaluated short-circuit'ed # IOW: CMake if() suxx. # Starting with CMake 2.6.3 you can group if expressions with (), but we # don't require 2.6.3 but 2.6.2, we can't use it. Alex set( _seem_to_have_cached_gpgme false ) if ( GPGME_INCLUDES ) if ( GPGME_VANILLA_LIBRARIES OR GPGME_QT_LIBRARIES OR GPGME_GLIB_LIBRARIES ) set( _seem_to_have_cached_gpgme true ) endif() endif() if ( _seem_to_have_cached_gpgme ) macro_bool_to_bool( GPGME_VANILLA_LIBRARIES GPGME_VANILLA_FOUND ) macro_bool_to_bool( GPGME_GLIB_LIBRARIES GPGME_GLIB_FOUND ) macro_bool_to_bool( GPGME_QT_LIBRARIES GPGME_QT_FOUND ) # this would have been preferred: #set( GPGME_*_FOUND macro_bool_to_bool(GPGME_*_LIBRARIES) ) if ( GPGME_VANILLA_FOUND OR GPGME_GLIB_FOUND OR GPGME_QT_FOUND ) set( GPGME_FOUND true ) else() set( GPGME_FOUND false ) endif() else() # is this needed, of just unreflected cut'n'paste? # this isn't a KDE library, after all! - if( NOT KDEWIN32_FOUND ) - find_package( KDEWIN32 REQUIRED ) + if( NOT KDEWIN_FOUND ) + find_package( KDEWIN REQUIRED ) endif() set( GPGME_FOUND false ) set( GPGME_VANILLA_FOUND false ) set( GPGME_GLIB_FOUND false ) set( GPGME_QT_FOUND false ) find_path( GPGME_INCLUDES gpgme.h ${CMAKE_INCLUDE_PATH} ${CMAKE_INSTALL_PREFIX}/include ) find_library( _gpgme_vanilla_library NAMES gpgme libgpgme gpgme-11 libgpgme-11 PATHS ${CMAKE_LIBRARY_PATH} ${CMAKE_INSTALL_PREFIX}/lib ) find_library( _gpgme_glib_library NAMES gpgme-glib libgpgme-glib gpgme-glib-11 libgpgme-glib-11 PATHS ${CMAKE_LIBRARY_PATH} ${CMAKE_INSTALL_PREFIX}/lib ) find_library( _gpgme_qt_library NAMES gpgme-qt libgpgme-qt gpgme-qt-11 libgpgme-qt-11 PATHS ${CMAKE_LIBRARY_PATH} ${CMAKE_INSTALL_PREFIX}/lib ) find_library( _gpg_error_library NAMES gpg-error libgpg-error gpg-error-0 libgpg-error-0 PATHS ${CMAKE_LIBRARY_PATH} ${CMAKE_INSTALL_PREFIX}/lib ) set( GPGME_INCLUDES ${GPGME_INCLUDES} ) if ( _gpgme_vanilla_library AND _gpg_error_library ) set( GPGME_VANILLA_LIBRARIES ${_gpgme_vanilla_library} ${_gpg_error_library} ) set( GPGME_VANILLA_FOUND true ) set( GPGME_FOUND true ) endif() if ( _gpgme_glib_library AND _gpg_error_library ) set( GPGME_GLIB_LIBRARIES ${_gpgme_glib_library} ${_gpg_error_library} ) set( GPGME_GLIB_FOUND true ) set( GPGME_FOUND true ) endif() if ( _gpgme_qt_library AND _gpg_error_library ) set( GPGME_QT_LIBRARIES ${_gpgme_qt_library} ${_gpg_error_library} ) set( GPGME_QT_FOUND true ) set( GPGME_FOUND true ) endif() endif() # these are Unix-only: set( GPGME_PTHREAD_FOUND false ) set( GPGME_PTH_FOUND false ) set( HAVE_GPGME_PTHREAD 0 ) set( HAVE_GPGME_PTH 0 ) macro_bool_to_01( GPGME_FOUND HAVE_GPGME ) macro_bool_to_01( GPGME_VANILLA_FOUND HAVE_GPGME_VANILLA ) macro_bool_to_01( GPGME_GLIB_FOUND HAVE_GPGME_GLIB ) macro_bool_to_01( GPGME_QT_FOUND HAVE_GPGME_QT ) else() # not WIN32 # On *nix, we have the gpgme-config script which can tell us all we # need to know: # see WIN32 case for an explanation of what this does: set( _seem_to_have_cached_gpgme false ) if ( GPGME_INCLUDES ) if ( GPGME_VANILLA_LIBRARIES OR GPGME_PTHREAD_LIBRARIES OR GPGME_PTH_LIBRARIES ) set( _seem_to_have_cached_gpgme true ) endif() endif() if ( _seem_to_have_cached_gpgme ) macro_bool_to_bool( GPGME_VANILLA_LIBRARIES GPGME_VANILLA_FOUND ) macro_bool_to_bool( GPGME_PTHREAD_LIBRARIES GPGME_PTHREAD_FOUND ) macro_bool_to_bool( GPGME_PTH_LIBRARIES GPGME_PTH_FOUND ) if ( GPGME_VANILLA_FOUND OR GPGME_PTHREAD_FOUND OR GPGME_PTH_FOUND ) set( GPGME_FOUND true ) else() set( GPGME_FOUND false ) endif() else() set( GPGME_FOUND false ) set( GPGME_VANILLA_FOUND false ) set( GPGME_PTHREAD_FOUND false ) set( GPGME_PTH_FOUND false ) find_program( _GPGMECONFIG_EXECUTABLE NAMES gpgme-config ) # if gpgme-config has been found if ( _GPGMECONFIG_EXECUTABLE ) message( STATUS "Found gpgme-config at ${_GPGMECONFIG_EXECUTABLE}" ) exec_program( ${_GPGMECONFIG_EXECUTABLE} ARGS --version OUTPUT_VARIABLE GPGME_VERSION ) set( _GPGME_MIN_VERSION "1.0.0" ) macro_ensure_version( ${_GPGME_MIN_VERSION} ${GPGME_VERSION} _GPGME_INSTALLED_VERSION_OK ) if ( NOT _GPGME_INSTALLED_VERSION_OK ) message( STATUS "The installed version of gpgme is too old: ${GPGME_VERSION} (required: >= ${_GPGME_MIN_VERSION})" ) else() message( STATUS "Found gpgme v${GPGME_VERSION}, checking for flavours..." ) exec_program( ${_GPGMECONFIG_EXECUTABLE} ARGS --libs OUTPUT_VARIABLE _gpgme_config_vanilla_libs RETURN_VALUE _ret ) if ( _ret ) set( _gpgme_config_vanilla_libs ) endif() exec_program( ${_GPGMECONFIG_EXECUTABLE} ARGS --thread=pthread --libs OUTPUT_VARIABLE _gpgme_config_pthread_libs RETURN_VALUE _ret ) if ( _ret ) set( _gpgme_config_pthread_libs ) endif() exec_program( ${_GPGMECONFIG_EXECUTABLE} ARGS --thread=pth --libs OUTPUT_VARIABLE _gpgme_config_pth_libs RETURN_VALUE _ret ) if ( _ret ) set( _gpgme_config_pth_libs ) endif() # append -lgpg-error to the list of libraries, if necessary foreach ( _flavour vanilla pthread pth ) if ( _gpgme_config_${_flavour}_libs AND NOT _gpgme_config_${_flavour}_libs MATCHES "lgpg-error" ) set( _gpgme_config_${_flavour}_libs "${_gpgme_config_${_flavour}_libs} -lgpg-error" ) endif() endforeach() if ( _gpgme_config_vanilla_libs OR _gpgme_config_pthread_libs OR _gpgme_config_pth_libs ) exec_program( ${_GPGMECONFIG_EXECUTABLE} ARGS --cflags OUTPUT_VARIABLE _GPGME_CFLAGS ) if ( _GPGME_CFLAGS ) string( REGEX REPLACE "(\r?\n)+$" " " _GPGME_CFLAGS "${_GPGME_CFLAGS}" ) string( REGEX REPLACE " *-I" ";" GPGME_INCLUDES "${_GPGME_CFLAGS}" ) endif() foreach ( _flavour vanilla pthread pth ) if ( _gpgme_config_${_flavour}_libs ) set( _gpgme_library_dirs ) set( _gpgme_library_names ) string( TOUPPER "${_flavour}" _FLAVOUR ) string( REGEX REPLACE " +" ";" _gpgme_config_${_flavour}_libs "${_gpgme_config_${_flavour}_libs}" ) foreach( _flag ${_gpgme_config_${_flavour}_libs} ) if ( "${_flag}" MATCHES "^-L" ) string( REGEX REPLACE "^-L" "" _dir "${_flag}" ) file( TO_CMAKE_PATH "${_dir}" _dir ) set( _gpgme_library_dirs ${_gpgme_library_dirs} "${_dir}" ) elseif( "${_flag}" MATCHES "^-l" ) string( REGEX REPLACE "^-l" "" _name "${_flag}" ) set( _gpgme_library_names ${_gpgme_library_names} "${_name}" ) endif() endforeach() set( GPGME_${_FLAVOUR}_FOUND true ) foreach( _name ${_gpgme_library_names} ) set( _gpgme_${_name}_lib ) # if -L options were given, look only there if ( _gpgme_library_dirs ) find_library( _gpgme_${_name}_lib NAMES ${_name} PATHS ${_gpgme_library_dirs} NO_DEFAULT_PATH ) endif() # if not found there, look in system directories if ( NOT _gpgme_${_name}_lib ) find_library( _gpgme_${_name}_lib NAMES ${_name} ) endif() # if still not found, then the whole flavour isn't found if ( NOT _gpgme_${_name}_lib ) if ( GPGME_${_FLAVOUR}_FOUND ) set( GPGME_${_FLAVOUR}_FOUND false ) set( _not_found_reason "dependant library ${_name} wasn't found" ) endif() endif() set( GPGME_${_FLAVOUR}_LIBRARIES ${GPGME_${_FLAVOUR}_LIBRARIES} "${_gpgme_${_name}_lib}" ) endforeach() #check_c_library_exists_explicit( gpgme gpgme_check_version "${_GPGME_CFLAGS}" "${GPGME_LIBRARIES}" GPGME_FOUND ) if ( GPGME_${_FLAVOUR}_FOUND ) message( STATUS " Found flavour '${_flavour}', checking whether it's usable...yes" ) else() message( STATUS " Found flavour '${_flavour}', checking whether it's usable...no" ) message( STATUS " (${_not_found_reason})" ) endif() endif() endforeach( _flavour ) # ensure that they are cached # This comment above doesn't make sense, the four following lines seem to do nothing. Alex set( GPGME_INCLUDES ${GPGME_INCLUDES} ) set( GPGME_VANILLA_LIBRARIES ${GPGME_VANILLA_LIBRARIES} ) set( GPGME_PTHREAD_LIBRARIES ${GPGME_PTHREAD_LIBRARIES} ) set( GPGME_PTH_LIBRARIES ${GPGME_PTH_LIBRARIES} ) if ( GPGME_VANILLA_FOUND OR GPGME_PTHREAD_FOUND OR GPGME_PTH_FOUND ) set( GPGME_FOUND true ) else() set( GPGME_FOUND false ) endif() endif() endif() endif() endif() # these are Windows-only: set( GPGME_GLIB_FOUND false ) set( GPGME_QT_FOUND false ) set( HAVE_GPGME_GLIB 0 ) set( HAVE_GPGME_QT 0 ) macro_bool_to_01( GPGME_FOUND HAVE_GPGME ) macro_bool_to_01( GPGME_VANILLA_FOUND HAVE_GPGME_VANILLA ) macro_bool_to_01( GPGME_PTHREAD_FOUND HAVE_GPGME_PTHREAD ) macro_bool_to_01( GPGME_PTH_FOUND HAVE_GPGME_PTH ) endif() # WIN32 | Unix set( _gpgme_flavours "" ) if ( GPGME_VANILLA_FOUND ) set( _gpgme_flavours "${_gpgme_flavours} vanilla" ) endif() if ( GPGME_GLIB_FOUND ) set( _gpgme_flavours "${_gpgme_flavours} Glib" ) endif() if ( GPGME_QT_FOUND ) set( _gpgme_flavours "${_gpgme_flavours} Qt" ) endif() if ( GPGME_PTHREAD_FOUND ) set( _gpgme_flavours "${_gpgme_flavours} pthread" ) endif() if ( GPGME_PTH_FOUND ) set( _gpgme_flavours "${_gpgme_flavours} pth" ) endif() # determine the library in one of the found flavours, can be reused e.g. by FindQgpgme.cmake, Alex foreach(_currentFlavour vanilla glib qt pth pthread) if(NOT GPGME_LIBRARY_DIR) get_filename_component(GPGME_LIBRARY_DIR "${_gpgme_${_currentFlavour}_lib}" PATH) endif() endforeach() if ( NOT Gpgme_FIND_QUIETLY ) if ( GPGME_FOUND ) message( STATUS "Usable gpgme flavours found: ${_gpgme_flavours}" ) else() message( STATUS "No usable gpgme flavours found." ) endif() macro_bool_to_bool( Gpgme_FIND_REQUIRED _req ) if ( WIN32 ) set( _gpgme_homepage "http://www.gpg4win.org" ) else() set( _gpgme_homepage "http://www.gnupg.org/related_software/gpgme" ) endif() macro_log_feature( GPGME_FOUND "gpgme" "GnuPG Made Easy Development Libraries" ${_gpgme_homepage} ${_req} "${_GPGME_MIN_VERSION} or greater" "Needed to provide GNU Privacy Guard support in KDE PIM applications. Necessary to compile many PIM application, including KMail." ) else() if ( Gpgme_FIND_REQUIRED AND NOT GPGME_FOUND ) message( FATAL_ERROR "Did not find GPGME" ) endif() endif() set( CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS_gpgme_saved ) diff --git a/kabc/plugins/dir/dir.desktop b/kabc/plugins/dir/dir.desktop index 0f9fdc51e..324501d07 100644 --- a/kabc/plugins/dir/dir.desktop +++ b/kabc/plugins/dir/dir.desktop @@ -1,55 +1,59 @@ [Desktop Entry] Name=Folder Name[ca]=Carpeta Name[da]=Mappe Name[de]=Ordner Name[el]=Φάκελος Name[es]=Carpeta Name[et]=Kataloog Name[fr]=Dossier Name[ga]=Fillteán Name[gl]=Cartafol Name[hu]=Mappa Name[it]=Cartella Name[km]=ថត Name[lv]=Mape Name[nb]=Mappe Name[nds]=Orner Name[nn]=Mappe Name[pt]=Pasta Name[pt_BR]=Pasta +Name[sr]=Фасцикла +Name[sr@latin]=Fascikla Name[sv]=Katalog Name[tr]=Dizin Name[uk]=Тека Name[x-test]=xxFolderxx Name[zh_CN]=文件夹 Name[zh_TW]=資料夾 Comment=Provides access to contacts, each stored in a single file, in a given folder. Supports standard VCard file and other formats depending on availability of plugins. Comment[ca]=Proporciona l'accés als contactes, cada un emmagatzemat en un fitxer individual, en una carpeta donada. Accepta els fitxers estàndard VCard i altres formats depenent de la disponibilitat dels connectors. Comment[da]=Giver adgang til kontakter, hver lagret i en enkelt fil, i en given mappe. Understøtter standard VCard-fil og andre formater afhængigt af tilgængeligheden af plugins. Comment[de]=Ermöglicht Zugriff auf Kontakte, die in einzelnen Dateien in einem vorgegebenen Ordner gespeichert sind. Unterstützt Standard-VCard-Dateien und andere Formate abhängig von den verfügbaren Modulen. Comment[el]=Προσφέρει πρόσβαση σε επαφές, αποθηκευμένες σε ξεχωριστά αρχεία, σε ένα 
δοσμένο φάκελο. Υποστηρίζει τυπικά αρχεία VCard και άλλες μορφές αρχείων ανάλογα με τη διαθεσιμότητα των πρόσθετων. Comment[es]=Provee acceso a los contactos, cada uno almacenado en un archivo diferente, dentro de un directorio determinado. Soporta archivos VCard estándar y otros formatos dependiendo de la disponibilidad de los complementos Comment[et]=Võimaldab kasutada eraldi failidesse salvestatud kontakte määratud kataloogis. Toetab standardseid VCard-faile ja teisi vorminguid sõltuvalt pluginate olemasolust. Comment[fr]=Fourni l'accès aux contacts stockés chacun dans un fichier dans le dossier indiqué. Le format VCard et d'autres formats sont pris en charge en fonction des modules externes disponibles Comment[ga]=Soláthraíonn sé seo rochtain ar theagmhálacha, gach ceann stóráilte i gcomhad aonair, i bhfillteán sonraithe. Tacaítear le comhaid v-Chárta agus le formáidí eile, ag brath ar na breiseáin atá ar fáil. Comment[gl]=Dá acceso aos contactos, cada un gardado nun ficheiro nun cartafol dado. Admite ficheiros vCard estándar e outros formatos, en función da dispoñibilidade de extensións. Comment[hu]=Névjegyek elérését biztosítja. Minden névjegy külön fájlban található, egy adott mappában. Támogatja a standard vCard formátumot, és bővítmények segítségével más formátumok is kezelhetők. Comment[it]=Fornisce accesso a contatti, ciascuno dei quali memorizzato in un singolo file in una cartella data. Gestisce file in standard VCard e altri formati in base ai plugin disponibili. Comment[km]=ផ្ដល់​ការ​ចូលដំណើរការ​ទៅ​កាន់​ទំនាក់ទំនង ដែល​ទំនាក់ទំនង​នីមួយៗ​ត្រូវ​បានផ្ទុកនៅ​ក្នុង​ឯកសារ​តែ​មួយ នៅ​ក្នុង​ថត​ដែល​បានផ្ដល់​ឲ្យ ។ គាំទ្រ​ឯកសារ VCard និង​ទ្រង់ទ្រាយ​ផ្សេងៗ​ ដោយ​អាស្រ័យ​លើ​កម្មវិធី​ជំនួយ​ដែល​មាន ។ Comment[lv]=Nodrošina pieeju kontaktiem, kas katrs saglabāts individuālā failā norādītajā mapē. Atbalsta standarta VCard failus un citus formātus, atkarībā no pieejamajiem spraudņiem. Comment[nb]=Gir tilgang til kontakter, lagret hver for seg i en enkelt fil, i en gitt mappe. Støtter standard vCard og andre formater avhengig av tilgjengelige programtillegg. Comment[nds]=Stellt Togriep op Kontakten praat, elkeen binnen een Datei binnen en angeven Orner. Ünnerstütt VCard-Dateien un anner Formaten na de verföögboren Modulen. Comment[nn]=Gjev tilgang til kontaktar i ei viss mappe, der dei er lagra i kvar sine filer. Standard vCard-filer er støtta, i tillegg til andre format, avhengig av kva programtillegg som er i bruk. Comment[pt]=Oferece o acesso aos contactos, estando cada um guardado num único ficheiro de uma dada pasta. Suporta os ficheiros VCard normais, assim como outros formatos, dependendo da disponibilidade dos 'plugins'. Comment[pt_BR]=Fornece acesso aos contatos, cada um armazenado em um único arquivo, em uma pasta informada. Suporta o formato de arquivos VCard padrão e outros formatos, dependendo da disponibilidade de plug-ins. +Comment[sr]=Пружа приступ контактима складиштеним у појединачним фајловима у датој фасцикли. Подржава стандардне в‑кард фајлове и друге формате, према расположивим прикључцима. +Comment[sr@latin]=Pruža pristup kontaktima skladištenim u pojedinačnim fajlovima u datoj fascikli. Podržava standardne vCard fajlove i druge formate, prema raspoloživim priključcima. Comment[sv]=Ger tillgång till kontakter, var och en lagrad i en enstaka fil, i en given katalog. Stöder vCard-standardfiler och andra format, beroende på tillgängliga insticksprogram. Comment[uk]=Надає доступ до контактів, кожен з яких зберігається у окремому файлі у вказаній теці. Підтримує стандартні файли VCard та файли у інших форматах, залежно від наявності відповідних додатків. Comment[x-test]=xxProvides access to contacts, each stored in a single file, in a given folder. Supports standard VCard file and other formats depending on availability of plugins.xx Comment[zh_CN]=提供对被存储在指定目录下的单独一个文件中的联系人的访问支持。支持标准 VCard 文件和其它插件所允许的格式。 Comment[zh_TW]=提供存取特定目錄下,個別儲存在單一檔案中的聯絡人的功能。支援標準的 VCard 檔,以及外掛程式所提供的其它格式檔案。 X-KDE-Library=kabc_directory Type=Service X-KDE-ServiceTypes=KResources/Plugin X-KDE-ResourceFamily=contact X-KDE-ResourceType=dir diff --git a/kabc/plugins/file/file.desktop b/kabc/plugins/file/file.desktop index d0892be23..a2c8158e8 100644 --- a/kabc/plugins/file/file.desktop +++ b/kabc/plugins/file/file.desktop @@ -1,72 +1,74 @@ [Desktop Entry] Name=File Name[be]=Файл Name[ca]=Fitxer Name[cs]=Soubor Name[da]=Fil Name[de]=Datei Name[el]=Αρχείο Name[es]=Archivo Name[et]=Fail Name[fr]=Fichier Name[ga]=Comhad Name[gl]=Ficheiro Name[hne]=फाइल Name[hu]=Fájl Name[ja]=ファイル Name[km]=ឯកសារ Name[lt]=Failas Name[lv]=Fails Name[nb]=Fil Name[nds]=Datei Name[nl]=Bestand Name[nn]=Fil Name[oc]=Fichièr Name[pa]=ਫਾਇਲ Name[pl]=Plik Name[pt]=Ficheiro Name[pt_BR]=Arquivo Name[ro]=Fișier Name[ru]=Файл Name[se]=Fiila Name[sk]=Súbor Name[sl]=Datoteka Name[sr]=Фајл Name[sr@latin]=Fajl Name[sv]=Fil Name[th]=แฟ้ม Name[tr]=Dosya Name[uk]=Файл Name[x-test]=xxFilexx Name[zh_CN]=文件 Name[zh_TW]=檔案 Comment=Provides access to contacts stored in a single local file. Supports standard VCard files and other formats depending on available plugins. Comment[ca]=Proporciona l'accés als contactes emmagatzemats en un fitxer individual. Accepta els fitxers estàndard VCard i altres formats, depenent dels connectors disponibles. Comment[da]=Giver adgang til kontakter, hver lagret i en enkelt fil. Understøtter standard VCard-fil og andre formater afhængigt af tilgængelige af plugins. Comment[de]=Ermöglicht Zugriff auf Kontakte, die in einer einzigen Dateien lokal gespeichert sind. Unterstützt Standard-VCard-Dateien und andere Formate abhängig von den verfügbaren Modulen. Comment[el]=Προσφέρει πρόσβαση σε επαφές, αποθηκευμένες σε ένα τοπικό αρχείο. Υποστηρίζει τυπικά αρχεία VCard και άλλες μορφές αρχείων ανάλογα με τη διαθεσιμότητα των πρόσθετων. Comment[es]=Provee acceso a los contactos almacenados en un único archivo local. Soporta archivos VCard estándar y otros formatos dependiendo de la disponibilidad de los componentes. Comment[et]=Võimaldab kasutada kohalikku faili salvestatud kontakte. Toetab standardseid VCard-faile ja teisi vorminguid sõltuvalt pluginate olemasolust. Comment[fr]=Fourni l'accès aux contacts stockés dans un seul fichier local. Le format VCard et d'autres formats sont pris en charge en fonction des modules externes disponibles. Comment[ga]=Soláthraíonn sé seo rochtain ar theagmhálacha, stóráilte i gcomhad aonair. Tacaítear le comhaid v-Chárta agus formáidí eile, ag brath ar na breiseáin atá ar fáil. Comment[gl]=Dá acceso a contactos gardados nun único ficheiro local. Admite ficheiros vCard estándar e outros formatos en función das extensións dispoñíbeis. Comment[hu]=Névjegyek elérését biztosítja. Minden névjegy egy közös helyi fájlban található. Támogatja a standard vCard formátumot, és bővítmények segítségével más formátumok is kezelhetők. Comment[it]=Fornisce accesso a contatti memorizzati in un singolo file locale. Gestisce file in standard VCard e altri formati in base ai plugin disponibili. Comment[ja]=単一のローカルファイルに保存されている連絡先へのアクセスを提供します。標準の VCard ファイルと、利用可能なプラグインに応じたフォーマットをサポートします。 Comment[km]=ផ្ដល់​កា​រចូលដំណើរការ​ទៅ​ទំនាក់ទំនង​​នៅ​ក្នុង​ឯកសារ​មូលដ្ឋាន​តែ​មួយ ។ គាំទ្រ​ឯកសារ VCard ស្តង់ដារ និង​ទ្រង់ទ្រាយ​ផ្សេងៗ​ ដោយ​អាស្រ័យ​លើ​កម្មវិធី​ជំនួយ​ដែល​មាន ។ Comment[lv]=Nodrošina piekļuvi kontaktiem, kas glabājas vienā lokālā failā. Atbalsta standarta VCard failus un citus formātus, atkarībā no pieejamajiem spraudņiem. Comment[nb]=Gir tilgang til kontakter, lagret i en enkelt lokal fil. Støtter standard vCard og andre formater avhengig av tilgjengelige programtillegg. Comment[nds]=Stellt Togriep op Kontakten praat, de in een enkel lokaal Datei wohrt warrt. Ünnerstütt VCard-Dateien un anner Formaten na de 'verföögboren Modulen. Comment[nn]=Gjev tilgang til kontaktar lagra i ei einskild lokal fil. Standard vCard-filer er støtta, i tillegg til andre format, avhengig av kva programtillegg som er i bruk. Comment[pt]=Oferece o acesso aos contactos, estando todos guardados num único ficheiro local. Suporta os ficheiros VCard normais, assim como outros formatos, dependendo da disponibilidade dos 'plugins'. Comment[pt_BR]=Fornece acesso aos contatos armazenados em um único arquivo local. Suporta o formato de arquivos VCard padrão e outros formatos, dependendo da disponibilidade de plug-ins. +Comment[sr]=Пружа приступ контактима складиштеним у једном локалном фајлу. Подржава стандардне в‑кард фајлове и друге формате, према расположивим прикључцима. +Comment[sr@latin]=Pruža pristup kontaktima skladištenim u jednom lokalnom fajlu. Podržava standardne vCard fajlove i druge formate, prema raspoloživim priključcima. Comment[sv]=Ger tillgång till kontakter lagrade i en enda lokal fil. Stöder vCard-standardfiler och andra format, beroende på tillgängliga insticksprogram. Comment[uk]=Надає доступ до контактів, які зберігаються у окремому локальному файлі. Підтримує стандартні файли VCard та файли у інших форматах, залежно від наявності відповідних додатків. Comment[x-test]=xxProvides access to contacts stored in a single local file. Supports standard VCard files and other formats depending on available plugins.xx Comment[zh_CN]=提供对被存储在单独的本地文件中的联系人的访问支持。支持标准 VCard 文件和其它插件所允许的格式。 Comment[zh_TW]=提供存取儲存在單一檔案中的聯絡人的功能。支援標準的 VCard 檔,以及外掛程式所提供的其它格式檔案。 X-KDE-Library=kabc_file Type=Service X-KDE-ServiceTypes=KResources/Plugin X-KDE-ResourceFamily=contact X-KDE-ResourceType=file diff --git a/kabc/plugins/net/net.desktop b/kabc/plugins/net/net.desktop index f7e0052cc..92e5290da 100644 --- a/kabc/plugins/net/net.desktop +++ b/kabc/plugins/net/net.desktop @@ -1,74 +1,76 @@ [Desktop Entry] Name=Network Name[be]=Сетка Name[ca]=Xarxa Name[cs]=Síť Name[da]=Netværk Name[de]=Netzwerk Name[el]=Δίκτυο Name[es]=Red Name[et]=Võrk Name[fr]=Réseau Name[ga]=Líonra Name[gl]=Rede Name[hne]=नेटवर्क Name[hu]=Hálózat Name[it]=Rete Name[ja]=ネットワーク Name[km]=បណ្តាញ Name[lt]=Tinklas Name[lv]=Tīkls Name[nb]=Nettverk Name[nds]=Nettwark Name[nl]=Netwerk Name[nn]=Nettverk Name[oc]=Ret Name[pa]=ਨੈੱਟਵਰਕ Name[pl]=Sieć Name[pt]=Rede Name[pt_BR]=Rede Name[ro]=Rețea Name[ru]=Сетевой файл Name[se]=Fierbmi Name[sk]=Sieť Name[sl]=Omrežje Name[sr]=Мрежа Name[sr@latin]=Mreža Name[sv]=Nätverk Name[th]=ระบบเครือข่าย Name[tr]=Ağ Name[uk]=Мережа Name[x-test]=xxNetworkxx Name[zh_CN]=网络 Name[zh_TW]=網路 Comment=Provides access to contacts in remote files using KDE's network framework KIO. Supports standard VCard files and other formats depending on available plugins. Comment[ca]=Proporciona l'accés als contactes en fitxers remots usant la infraestructura KIO de xarxa del KDE. Accepta els fitxers estàndard VCard i altres formats, depenent dels connectors disponibles. Comment[da]=Giver adgang til kontakter i eksterne filer med brug af KDE's netværks-framework KIO. Understøtter standard VCard-filer og andre formater afhængigt af tilgængelige plugins. Comment[de]=Ermöglicht Zugriff auf Kontakte in entfernten Dateien durch das KIO-Netzwerksystem von KDE. Unterstützt Standard-VCard-Dateien und andere Formate abhängig von den verfügbaren Modulen. Comment[el]=Προσφέρει πρόσβαση σε επαφές σε απομακρυσμένα αρχεία με τη χρήση του συστήματος KIO του KDE. Υποστηρίζει τυπικά αρχεία VCard και άλλες μορφές αρχείων ανάλογα με τη διαθεσιμότητα των πρόσθετων. Comment[es]=Provee acceso a los contactos en un archivo remoto utilizando la infraestructura de red KIO de KDE. Soporta archivos VCard estándar y otros formatos dependiendo en la disponibilidad de los complementos. Comment[et]=Võimaldab kasutada võrgufaile KDE võrguraamistiku KIO abil. Toetab standardseid VCard-faile ja teisi vorminguid sõltuvalt pluginate olemasolust. Comment[fr]=Fourni l'accès aux contacts stockés dans des fichiers distants en utilisant le mécanisme réseau KIO de KDE. Le format VCard et d'autres formats sont pris en charge en fonction des modules externes disponibles. Comment[ga]=Soláthraíonn sé seo rochtain ar theagmhálacha i gcianchomhaid tríd an gcreatlach líonra KIO atá cuid de KDE. Tacaítear le comhaid v-Chárta agus formáidí eile, ag brath ar na breiseáin atá ar fáil. Comment[gl]=Dá acceso a contactos gardados en ficheiros remotos mediante a infraestrutura de rede KIO, de KDE. Admite ficheiros vCard estándar e outros formatos en función das extensións dispoñíbeis. Comment[hu]=Távoli fájlokban található névjegyek elérését biztosítja a KDE KIO keretrendszeren keresztül. Támogatja a standard vCard formátumot, és bővítmények segítségével más formátumok is kezelhetők. Comment[it]=Fornisce accesso a contatti su file remoti usando KIO, l'infrastruttura di rete di KDE. Gestisce file in standard VCard e altri formati in base ai plugin disponibili. Comment[ja]=KDE のネットワークフレームワーク KIO を使って、リモートファイルに保存されている連絡先へのアクセスを提供します。標準の VCard ファイルと、利用可能なプラグインに応じたフォーマットをサポートします。 Comment[km]=ផ្ដល់​កា​រចូលដំណើរការ​ទៅកាន់​ទំនាក់ទំនង​ក្នុង​​ឯកសារ​ពី​ចម្ងាយ ដោយ​ប្រើ KIO ​គ្រោងការណ៍​បណ្ដាញ​របស់ KDE ។ គាំទ្រ​ឯកសារ VCard ស្តង់ដារ និង​ទ្រង់ទ្រាយ​ផ្សេងៗ​ទៀត ដោយ​អាស្រ័យ​លើ​កម្មវិធី​ជំនួយ​ដែល​លមាន ។ Comment[lv]=Nodrošina piekļuvi kontaktiem attālinātos failus, izmantojot KDE tīklošanas ietvaru KIO. Atbalsta standarta VCard failus un citus formātus, atkarībā no pieejamajiem spraudņiem. Comment[nb]=Gir tilgang til kontakter i nettverksfiler, ved bruk av KDEs rammeverk KIO for nettverk. Støtter standard vCard og andre formater avhengig av tilgjengelige programtillegg. Comment[nds]=Stellt Togriep op Kontakten binnen feern Dateien praat, bruukt KDE sien Nettwark-Rahmenwark KIO. Ünnerstütt VCard-Dateien un anner Formaten na de verföögboren Modulen. Comment[nn]=Gjev tilgang til kontaktar i nettverksfiler ved å bruka KIO, nettverksrammeverket frå KDE. Standard vCard-filer er støtta, i tillegg til andre format, avhengig av kva programtillegg som er i bruk. Comment[pt]=Oferece o acesso aos contactos em ficheiros remotos, disponíveis através de um KIO da plataforma de rede do KDE. Suporta os ficheiros VCard normais, assim como outros formatos, dependendo da disponibilidade dos 'plugins'. Comment[pt_BR]=Fornece acesso aos contatos em arquivos remotos usando o KIO framework da rede do KDE. O suporte à arquivos VCard padrão e outros formatos dependem da disponibilidade de plug-ins. +Comment[sr]=Пружа приступ контактима у удаљеним фајловима преко КДЕ‑овог мрежног радног оквира К‑У/И. Подржава стандардне в‑кард фајлове и друге формате, према расположивим прикључцима. +Comment[sr@latin]=Pruža pristup kontaktima u udaljenim fajlovima preko KDE‑ovog mrežnog radnog okvira K‑U/I. Podržava standardne vCard fajlove i druge formate, prema raspoloživim priključcima. Comment[sv]=Ger tillgång till kontakter i fjärrfiler med användning av KDE:s I/O-ramverk. Stöder vCard-standardfiler och andra format, beroende på tillgängliga insticksprogram. Comment[tr]=KDE'nin ağ iskeleti KIO'yu kullanarak uzak dosyalardaki kişilere erişim sağlar. Mevcut eklentilere bağlı olarak standart VCard dosyalarını ve diğer dosya biçimleri destekler. Comment[uk]=Надає доступ до контактів, які зберігаються у віддалених файлах, за допомогою мережевих засобів KIO KDE. Підтримує стандартні файли VCard та файли у інших форматах, залежно від наявності відповідних додатків. Comment[x-test]=xxProvides access to contacts in remote files using KDE's network framework KIO. Supports standard VCard files and other formats depending on available plugins.xx Comment[zh_CN]=通过 KDE 的网络透明框架 KIO,提供对被存储在远程文件中的联系人的访问支持。支持标准 VCard 文件和其它插件所允许的格式。 Comment[zh_TW]=提供存取使用 KDE 網路架構 KIO 儲存在遠端檔案中的聯絡人。支援標準的 VCard 檔,以及外掛程式所提供的其它格式檔案。 X-KDE-Library=kabc_net Type=Service X-KDE-ServiceTypes=KResources/Plugin X-KDE-ResourceFamily=contact X-KDE-ResourceType=net diff --git a/kcal/CMakeLists.txt b/kcal/CMakeLists.txt index 541eac141..a7472ab79 100644 --- a/kcal/CMakeLists.txt +++ b/kcal/CMakeLists.txt @@ -1,184 +1,184 @@ project(kcal) set(LIBICAL_MIN_VERSION "0.42") find_package(Libical) -macro_log_feature(LIBICAL_FOUND "libical" "Reference implementation of the iCalendar data type and serialization format" "http://sourceforge.net/projects/freeassociation" TRUE ${LIBICAL_MIN_VERSION} "Required for the critical PIM kcal library.") +macro_log_feature(LIBICAL_FOUND "libical" "Reference implementation of the iCalendar data type and serialization format" "http://sourceforge.net/projects/freeassociation" TRUE "${LIBICAL_MIN_VERSION}" "Required for the critical PIM kcal library.") add_definitions(-DKDE_DEFAULT_DEBUG_AREA=5800) include (ConfigureChecks.cmake) if(KDE4_BUILD_TESTS) add_definitions(-DCOMPILING_TESTS) endif(KDE4_BUILD_TESTS) include_directories( ${LIBICAL_INCLUDE_DIRS} ${LIBICAL_INCLUDE_DIRS}/libical ${CMAKE_CURRENT_SOURCE_DIR}/versit ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/kabc ${CMAKE_BINARY_DIR}/kabc ${KDE4_INCLUDE_DIR} ) set(libversit_SRCS ${CMAKE_SOURCE_DIR}/kcal/versit/vcc.c ${CMAKE_SOURCE_DIR}/kcal/versit/vobject.c ) ########### next target ############### set(kcal_LIB_SRCS ${libversit_SRCS} incidencebase.cpp incidence.cpp journal.cpp todo.cpp event.cpp freebusy.cpp freebusyperiod.cpp attendee.cpp attachment.cpp recurrencerule.cpp recurrence.cpp alarm.cpp customproperties.cpp calendar.cpp calendarlocal.cpp calformat.cpp vcalformat.cpp icalformat.cpp icalformat_p.cpp incidenceformatter.cpp vcaldrag.cpp icaldrag.cpp exceptions.cpp scheduler.cpp imipscheduler.cpp dummyscheduler.cpp calfilter.cpp person.cpp period.cpp duration.cpp dndfactory.cpp calstorage.cpp filestorage.cpp compat.cpp resourcecalendar.cpp resourcelocal.cpp resourcelocalconfig.cpp resourcelocaldir.cpp resourcelocaldirconfig.cpp resourcecached.cpp resourcecachedconfig.cpp calendarresources.cpp qtopiaformat.cpp htmlexport.cpp calendarnull.cpp freebusyurlstore.cpp confirmsavedialog.cpp icaltimezones.cpp kresult.cpp assignmentvisitor.cpp comparisonvisitor.cpp ) kde4_add_kcfg_files(kcal_LIB_SRCS htmlexportsettings.kcfgc ) kde4_add_library(kcal SHARED ${kcal_LIB_SRCS}) target_link_libraries(kcal ${KDE4_KDEUI_LIBS} ${KDE4_KIO_LIBS} ${QT_QTXML_LIBRARY} kresources kabc kpimutils ${LIBICAL_LIBRARIES}) target_link_libraries(kcal LINK_INTERFACE_LIBRARIES kresources kabc) set_target_properties(kcal PROPERTIES VERSION ${GENERIC_LIB_VERSION} SOVERSION ${GENERIC_LIB_SOVERSION}) install(TARGETS kcal EXPORT kdepimlibsLibraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS}) ########### next target ############### set(kcal_local_PART_SRCS resourcelocal_plugin.cpp ) kde4_add_plugin(kcal_local ${kcal_local_PART_SRCS}) target_link_libraries(kcal_local ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} kcal kresources ) install(TARGETS kcal_local DESTINATION ${PLUGIN_INSTALL_DIR}) ########### next target ############### set(kcal_localdir_PART_SRCS resourcelocaldir_plugin.cpp ) kde4_add_plugin(kcal_localdir ${kcal_localdir_PART_SRCS}) target_link_libraries(kcal_localdir ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} kcal kresources) install(TARGETS kcal_localdir DESTINATION ${PLUGIN_INSTALL_DIR}) add_subdirectory( tests ) ########### install files ############### install( FILES local.desktop localdir.desktop DESTINATION ${SERVICES_INSTALL_DIR}/kresources/kcal) install( FILES alarm.h assignmentvisitor.h attachment.h attendee.h calendar.h calendarlocal.h calendarnull.h calendarresources.h calfilter.h calformat.h calstorage.h comparisonvisitor.h confirmsavedialog.h customproperties.h dndfactory.h duration.h event.h exceptions.h filestorage.h freebusy.h freebusycache.h freebusyperiod.h freebusyurlstore.h ${CMAKE_CURRENT_BINARY_DIR}/htmlexportsettings.h htmlexport.h icaldrag.h icalformat.h icaltimezones.h imipscheduler.h incidencebase.h incidence.h incidenceformatter.h journal.h kcal_export.h kcalversion.h listbase.h period.h person.h qtopiaformat.h recurrencerule.h recurrence.h resourcecached.h resourcecachedconfig.h resourcecalendar.h resourcelocalconfig.h resourcelocaldirconfig.h resourcelocaldir.h resourcelocal.h scheduler.h sortablelist.h todo.h vcaldrag.h vcalformat.h kresult.h DESTINATION ${INCLUDE_INSTALL_DIR}/kcal COMPONENT Devel) install( FILES kcal_manager.desktop DESTINATION ${SERVICES_INSTALL_DIR}/kresources) diff --git a/kcal/customproperties.cpp b/kcal/customproperties.cpp index 61ebc7641..fb486e607 100644 --- a/kcal/customproperties.cpp +++ b/kcal/customproperties.cpp @@ -1,190 +1,184 @@ /* This file is part of the kcal library. Copyright (c) 2002,2006 David Jarvie This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** @file This file is part of the API for handling calendar data and defines the CustomProperties class. @brief A class to manage custom calendar properties. @author David Jarvie \ */ #include "customproperties.h" #include #include using namespace KCal; //@cond PRIVATE static bool checkName( const QByteArray &name ); class CustomProperties::Private { public: bool operator==( const Private &other ) const; QMap mProperties; // custom calendar properties }; bool CustomProperties::Private::operator==( const CustomProperties::Private &other ) const { if ( mProperties.count() != other.mProperties.count() ) { return false; } for ( QMap::ConstIterator it = mProperties.begin(); it != mProperties.end(); ++it ) { QMap::ConstIterator itOther = other.mProperties.find( it.key() ); if ( itOther == other.mProperties.end() || itOther.value() != it.value() ) { return false; } } return true; } //@endcond CustomProperties::CustomProperties() : d( new Private ) { } CustomProperties::CustomProperties( const CustomProperties &cp ) : d( new Private( *cp.d ) ) { } CustomProperties &CustomProperties::operator=( const CustomProperties &other ) { // check for self assignment if ( &other == this ) { return *this; } *d = *other.d; return *this; } CustomProperties::~CustomProperties() { delete d; } bool CustomProperties::operator==( const CustomProperties &other ) const { return *d == *other.d; } void CustomProperties::setCustomProperty( const QByteArray &app, const QByteArray &key, const QString &value ) { if ( value.isNull() || key.isEmpty() || app.isEmpty() ) { return; } QByteArray property = "X-KDE-" + app + '-' + key; if ( !checkName( property ) ) { return; } d->mProperties[property] = value; customPropertyUpdated(); } void CustomProperties::removeCustomProperty( const QByteArray &app, const QByteArray &key ) { removeNonKDECustomProperty( QByteArray( "X-KDE-" + app + '-' + key ) ); } QString CustomProperties::customProperty( const QByteArray &app, const QByteArray &key ) const { return nonKDECustomProperty( QByteArray( "X-KDE-" + app + '-' + key ) ); } void CustomProperties::setNonKDECustomProperty( const QByteArray &name, const QString &value ) { if ( value.isNull() || !checkName( name ) ) { return; } d->mProperties[name] = value; customPropertyUpdated(); } void CustomProperties::removeNonKDECustomProperty( const QByteArray &name ) { - QMap::Iterator it = d->mProperties.find( name ); - if ( it != d->mProperties.end() ) { - d->mProperties.erase( it ); + if ( d->mProperties.remove( name ) ) { customPropertyUpdated(); } } QString CustomProperties::nonKDECustomProperty( const QByteArray &name ) const { - QMap::ConstIterator it = d->mProperties.constFind( name ); - if ( it == d->mProperties.constEnd() ) { - return QString(); - } - return it.value(); + return d->mProperties.value( name ); } void CustomProperties::setCustomProperties( const QMap &properties ) { bool changed = false; for ( QMap::ConstIterator it = properties.begin(); it != properties.end(); ++it ) { // Validate the property name and convert any null string to empty string if ( checkName( it.key() ) ) { d->mProperties[it.key()] = it.value().isNull() ? QString( "" ) : it.value(); changed = true; } } if ( changed ) { customPropertyUpdated(); } } QMap CustomProperties::customProperties() const { return d->mProperties; } //@cond PRIVATE bool checkName( const QByteArray &name ) { // Check that the property name starts with 'X-' and contains // only the permitted characters const char *n = name; int len = name.length(); if ( len < 2 || n[0] != 'X' || n[1] != '-' ) { return false; } for ( int i = 2; i < len; ++i ) { char ch = n[i]; if ( ( ch >= 'A' && ch <= 'Z' ) || ( ch >= 'a' && ch <= 'z' ) || ( ch >= '0' && ch <= '9' ) || ch == '-' ) { continue; } return false; // invalid character found } return true; } //@endcond diff --git a/kcal/icalformat_p.cpp b/kcal/icalformat_p.cpp index 1b41fdd4a..5e232b740 100644 --- a/kcal/icalformat_p.cpp +++ b/kcal/icalformat_p.cpp @@ -1,2656 +1,2671 @@ /* This file is part of the kcal library. Copyright (c) 2001 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer Copyright (c) 2006 David Jarvie This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** @file This file is part of the API for handling calendar data and defines the internal ICalFormat classes. @brief This class provides the libical dependent functions for ICalFormat. @author Cornelius Schumacher \ @author Reinhold Kainhofer \ @author David Jarvie \ */ #include "icalformat_p.h" #include "icalformat.h" #include "icaltimezones.h" #include "calendar.h" #include "compat.h" #include "journal.h" extern "C" { #include #include #include } #include #include #include #include #include #include #include #include #include #include #include using namespace KCal; /* Static helpers */ /* static void _dumpIcaltime( const icaltimetype& t) { kDebug() << "--- Y:" << t.year << "M:" << t.month << "D:" << t.day; kDebug() << "--- H:" << t.hour << "M:" << t.minute << "S:" << t.second; kDebug() << "--- isUtc:" << icaltime_is_utc( t ); kDebug() << "--- zoneId:" << icaltimezone_get_tzid( const_cast( t.zone ) ); } */ //@cond PRIVATE static QString quoteForParam( const QString &text ) { QString tmp = text; tmp.remove( '"' ); if ( tmp.contains( ';' ) || tmp.contains( ':' ) || tmp.contains( ',' ) ) { return tmp; // libical quotes in this case already, see icalparameter_as_ical_string() } return QString::fromLatin1( "\"" ) + tmp + QString::fromLatin1( "\"" ); } const int gSecondsPerMinute = 60; const int gSecondsPerHour = gSecondsPerMinute * 60; const int gSecondsPerDay = gSecondsPerHour * 24; const int gSecondsPerWeek = gSecondsPerDay * 7; class ToComponentVisitor : public IncidenceBase::Visitor { public: ToComponentVisitor( ICalFormatImpl *impl, iTIPMethod m ) : mImpl( impl ), mComponent( 0 ), mMethod( m ) {} bool visit( Event *e ) { mComponent = mImpl->writeEvent( e ); return true; } bool visit( Todo *e ) { mComponent = mImpl->writeTodo( e ); return true; } bool visit( Journal *e ) { mComponent = mImpl->writeJournal( e ); return true; } bool visit( FreeBusy *fb ) { mComponent = mImpl->writeFreeBusy( fb, mMethod ); return true; } icalcomponent *component() { return mComponent; } private: ICalFormatImpl *mImpl; icalcomponent *mComponent; iTIPMethod mMethod; }; class ICalFormatImpl::Private { public: Private( ICalFormatImpl *impl, ICalFormat *parent ) : mImpl( impl ), mParent( parent ), mCompat( new Compat ) {} ~Private() { delete mCompat; } void writeIncidenceBase( icalcomponent *parent, IncidenceBase * ); void readIncidenceBase( icalcomponent *parent, IncidenceBase * ); void writeCustomProperties( icalcomponent *parent, CustomProperties * ); void readCustomProperties( icalcomponent *parent, CustomProperties * ); ICalFormatImpl *mImpl; ICalFormat *mParent; QString mLoadedProductId; // PRODID string loaded from calendar file Event::List mEventsRelate; // events with relations Todo::List mTodosRelate; // todos with relations Compat *mCompat; }; //@endcond inline icaltimetype ICalFormatImpl::writeICalUtcDateTime ( const KDateTime &dt ) { return writeICalDateTime( dt.toUtc() ); } ICalFormatImpl::ICalFormatImpl( ICalFormat *parent ) : d( new Private( this, parent ) ) { } ICalFormatImpl::~ICalFormatImpl() { delete d; } QString ICalFormatImpl::loadedProductId() const { return d->mLoadedProductId; } icalcomponent *ICalFormatImpl::writeIncidence( IncidenceBase *incidence, iTIPMethod method ) { ToComponentVisitor v( this, method ); if ( incidence->accept(v) ) { return v.component(); } else { return 0; } } icalcomponent *ICalFormatImpl::writeTodo( Todo *todo, ICalTimeZones *tzlist, ICalTimeZones *tzUsedList ) { QString tmpStr; QStringList tmpStrList; icalcomponent *vtodo = icalcomponent_new( ICAL_VTODO_COMPONENT ); writeIncidence( vtodo, todo, tzlist, tzUsedList ); // due date icalproperty *prop; if ( todo->hasDueDate() ) { icaltimetype due; if ( todo->allDay() ) { due = writeICalDate( todo->dtDue( true ).date() ); prop = icalproperty_new_due(due); } else { prop = writeICalDateTimeProperty( ICAL_DUE_PROPERTY, todo->dtDue(true), tzlist, tzUsedList ); } icalcomponent_add_property( vtodo, prop ); } // start time if ( todo->hasStartDate() ) { icaltimetype start; if ( todo->allDay() ) { start = writeICalDate( todo->dtStart( true ).date() ); prop = icalproperty_new_dtstart( start ); } else { prop = writeICalDateTimeProperty( ICAL_DTSTART_PROPERTY, todo->dtStart( true ), tzlist, tzUsedList ); } icalcomponent_add_property( vtodo, prop ); } // completion date (UTC) if ( todo->isCompleted() ) { if ( !todo->hasCompletedDate() ) { // If the todo was created by KOrganizer<2.2 it does not have // a correct completion date. Set one now. todo->setCompleted( KDateTime::currentUtcDateTime() ); } icaltimetype completed = writeICalUtcDateTime( todo->completed() ); icalcomponent_add_property( vtodo, icalproperty_new_completed ( completed ) ); } icalcomponent_add_property( vtodo, icalproperty_new_percentcomplete( todo->percentComplete() ) ); if ( todo->recurs() ) { icalcomponent_add_property( vtodo, writeICalDateTimeProperty( ICAL_RECURRENCEID_PROPERTY, todo->dtDue(), tzlist, tzUsedList ) ); } return vtodo; } icalcomponent *ICalFormatImpl::writeEvent( Event *event, ICalTimeZones *tzlist, ICalTimeZones *tzUsedList ) { icalcomponent *vevent = icalcomponent_new( ICAL_VEVENT_COMPONENT ); writeIncidence( vevent, event, tzlist, tzUsedList ); // start time icalproperty *prop; icaltimetype start; if ( event->allDay() ) { start = writeICalDate( event->dtStart().date() ); prop = icalproperty_new_dtstart( start ); } else { prop = writeICalDateTimeProperty( ICAL_DTSTART_PROPERTY, event->dtStart(), tzlist, tzUsedList ); } icalcomponent_add_property( vevent, prop ); if ( event->hasEndDate() ) { // End time. // RFC2445 says that if DTEND is present, it has to be greater than DTSTART. icaltimetype end; KDateTime dt = event->dtEnd(); if ( event->allDay() ) { // +1 day because end date is non-inclusive. end = writeICalDate( dt.date().addDays( 1 ) ); icalcomponent_add_property( vevent, icalproperty_new_dtend(end) ); } else { if ( dt != event->dtStart() ) { icalcomponent_add_property( vevent, writeICalDateTimeProperty( ICAL_DTEND_PROPERTY, dt, tzlist, tzUsedList ) ); } } } // TODO: resources #if 0 // resources QStringList tmpStrList = anEvent->resources(); QString tmpStr = tmpStrList.join( ";" ); if ( !tmpStr.isEmpty() ) { addPropValue( vevent, VCResourcesProp, tmpStr.toUtf8() ); } #endif // Transparency switch( event->transparency() ) { case Event::Transparent: icalcomponent_add_property( vevent, icalproperty_new_transp( ICAL_TRANSP_TRANSPARENT ) ); break; case Event::Opaque: icalcomponent_add_property( vevent, icalproperty_new_transp( ICAL_TRANSP_OPAQUE ) ); break; } return vevent; } icalcomponent *ICalFormatImpl::writeFreeBusy( FreeBusy *freebusy, iTIPMethod method ) { icalcomponent *vfreebusy = icalcomponent_new( ICAL_VFREEBUSY_COMPONENT ); d->writeIncidenceBase( vfreebusy, freebusy ); icalcomponent_add_property( vfreebusy, icalproperty_new_dtstart( writeICalUtcDateTime( freebusy->dtStart() ) ) ); icalcomponent_add_property( vfreebusy, icalproperty_new_dtend( writeICalUtcDateTime( freebusy->dtEnd() ) ) ); if ( method == iTIPRequest ) { icalcomponent_add_property( vfreebusy, icalproperty_new_uid( freebusy->uid().toUtf8() ) ); } //Loops through all the periods in the freebusy object QList list = freebusy->busyPeriods(); icalperiodtype period = icalperiodtype_null_period(); for ( int i = 0, count = list.count(); i < count; ++i ) { period.start = writeICalUtcDateTime( list[i].start() ); if ( list[i].hasDuration() ) { period.duration = writeICalDuration( list[i].duration() ); } else { period.end = writeICalUtcDateTime( list[i].end() ); } icalcomponent_add_property( vfreebusy, icalproperty_new_freebusy( period ) ); } return vfreebusy; } icalcomponent *ICalFormatImpl::writeJournal( Journal *journal, ICalTimeZones *tzlist, ICalTimeZones *tzUsedList ) { icalcomponent *vjournal = icalcomponent_new( ICAL_VJOURNAL_COMPONENT ); writeIncidence( vjournal, journal, tzlist, tzUsedList ); // start time icalproperty *prop; KDateTime dt = journal->dtStart(); if ( dt.isValid() ) { icaltimetype start; if ( journal->allDay() ) { start = writeICalDate( dt.date() ); prop = icalproperty_new_dtstart( start ); } else { prop = writeICalDateTimeProperty( ICAL_DTSTART_PROPERTY, dt, tzlist, tzUsedList ); } icalcomponent_add_property( vjournal, prop ); } return vjournal; } void ICalFormatImpl::writeIncidence( icalcomponent *parent, Incidence *incidence, ICalTimeZones *tzlist, ICalTimeZones *tzUsedList ) { if ( incidence->schedulingID() != incidence->uid() ) { // We need to store the UID in here. The rawSchedulingID will // go into the iCal UID component incidence->setCustomProperty( "LIBKCAL", "ID", incidence->uid() ); } else { incidence->removeCustomProperty( "LIBKCAL", "ID" ); } d->writeIncidenceBase( parent, incidence ); // creation date icalcomponent_add_property( parent, writeICalDateTimeProperty( ICAL_CREATED_PROPERTY, incidence->created() ) ); // unique id // If the scheduling ID is different from the real UID, the real // one is stored on X-REALID above if ( !incidence->schedulingID().isEmpty() ) { icalcomponent_add_property( parent, icalproperty_new_uid( incidence->schedulingID().toUtf8() ) ); } // revision if ( incidence->revision() > 0 ) { // 0 is default, so don't write that out icalcomponent_add_property( parent, icalproperty_new_sequence( incidence->revision() ) ); } // last modification date if ( incidence->lastModified().isValid() ) { icalcomponent_add_property( parent, writeICalDateTimeProperty( ICAL_LASTMODIFIED_PROPERTY, incidence->lastModified() ) ); } // description if ( !incidence->description().isEmpty() ) { icalcomponent_add_property( parent, writeDescription( incidence->description(), incidence->descriptionIsRich() ) ); } // summary if ( !incidence->summary().isEmpty() ) { icalcomponent_add_property( parent, writeSummary( incidence->summary(), incidence->summaryIsRich() ) ); } // location if ( !incidence->location().isEmpty() ) { icalcomponent_add_property( parent, writeLocation( incidence->location(), incidence->locationIsRich() ) ); } // status icalproperty_status status = ICAL_STATUS_NONE; switch ( incidence->status() ) { case Incidence::StatusTentative: status = ICAL_STATUS_TENTATIVE; break; case Incidence::StatusConfirmed: status = ICAL_STATUS_CONFIRMED; break; case Incidence::StatusCompleted: status = ICAL_STATUS_COMPLETED; break; case Incidence::StatusNeedsAction: status = ICAL_STATUS_NEEDSACTION; break; case Incidence::StatusCanceled: status = ICAL_STATUS_CANCELLED; break; case Incidence::StatusInProcess: status = ICAL_STATUS_INPROCESS; break; case Incidence::StatusDraft: status = ICAL_STATUS_DRAFT; break; case Incidence::StatusFinal: status = ICAL_STATUS_FINAL; break; case Incidence::StatusX: { icalproperty *p = icalproperty_new_status( ICAL_STATUS_X ); icalvalue_set_x( icalproperty_get_value( p ), incidence->statusStr().toUtf8() ); icalcomponent_add_property( parent, p ); break; } case Incidence::StatusNone: default: break; } if ( status != ICAL_STATUS_NONE ) { icalcomponent_add_property( parent, icalproperty_new_status( status ) ); } // secrecy icalproperty_class secClass; switch ( incidence->secrecy() ) { case Incidence::SecrecyPublic: secClass = ICAL_CLASS_PUBLIC; break; case Incidence::SecrecyConfidential: secClass = ICAL_CLASS_CONFIDENTIAL; break; case Incidence::SecrecyPrivate: default: secClass = ICAL_CLASS_PRIVATE; break; } if ( secClass != ICAL_CLASS_PUBLIC ) { icalcomponent_add_property( parent, icalproperty_new_class( secClass ) ); } // geo if ( incidence->hasGeo() ) { icalgeotype geo; geo.lat = incidence->geoLatitude(); geo.lon = incidence->geoLongitude(); icalcomponent_add_property( parent, icalproperty_new_geo( geo ) ); } // priority if ( incidence->priority() > 0 ) { // 0 is undefined priority icalcomponent_add_property( parent, icalproperty_new_priority( incidence->priority() ) ); } // categories - QStringList categories = incidence->categories(); - QStringList::const_iterator it; - for ( it = categories.constBegin(); it != categories.constEnd(); ++it ) { + QString categories = incidence->categories().join( "," ); + if ( !categories.isEmpty() ) { icalcomponent_add_property( - parent, icalproperty_new_categories( (*it).toUtf8() ) ); + parent, icalproperty_new_categories( categories.toUtf8() ) ); } // related event if ( !incidence->relatedToUid().isEmpty() ) { icalcomponent_add_property( parent, icalproperty_new_relatedto( incidence->relatedToUid().toUtf8() ) ); } RecurrenceRule::List rrules( incidence->recurrence()->rRules() ); RecurrenceRule::List::ConstIterator rit; for ( rit = rrules.constBegin(); rit != rrules.constEnd(); ++rit ) { icalcomponent_add_property( parent, icalproperty_new_rrule( writeRecurrenceRule( (*rit) ) ) ); } RecurrenceRule::List exrules( incidence->recurrence()->exRules() ); RecurrenceRule::List::ConstIterator exit; for ( exit = exrules.constBegin(); exit != exrules.constEnd(); ++exit ) { icalcomponent_add_property( - parent, icalproperty_new_rrule( writeRecurrenceRule( (*exit) ) ) ); + parent, icalproperty_new_exrule( writeRecurrenceRule( (*exit) ) ) ); } DateList dateList = incidence->recurrence()->exDates(); DateList::ConstIterator exIt; for ( exIt = dateList.constBegin(); exIt != dateList.constEnd(); ++exIt ) { icalcomponent_add_property( parent, icalproperty_new_exdate( writeICalDate(*exIt) ) ); } DateTimeList dateTimeList = incidence->recurrence()->exDateTimes(); DateTimeList::ConstIterator extIt; for ( extIt = dateTimeList.constBegin(); extIt != dateTimeList.constEnd(); ++extIt ) { icalcomponent_add_property( parent, writeICalDateTimeProperty( ICAL_EXDATE_PROPERTY, *extIt, tzlist, tzUsedList ) ); } dateList = incidence->recurrence()->rDates(); DateList::ConstIterator rdIt; for ( rdIt = dateList.constBegin(); rdIt != dateList.constEnd(); ++rdIt ) { icalcomponent_add_property( parent, icalproperty_new_rdate( writeICalDatePeriod(*rdIt) ) ); } dateTimeList = incidence->recurrence()->rDateTimes(); DateTimeList::ConstIterator rdtIt; for ( rdtIt = dateTimeList.constBegin(); rdtIt != dateTimeList.constEnd(); ++rdtIt ) { icalcomponent_add_property( parent, writeICalDateTimeProperty( ICAL_RDATE_PROPERTY, *rdtIt, tzlist, tzUsedList ) ); } // attachments Attachment::List attachments = incidence->attachments(); Attachment::List::ConstIterator atIt; for ( atIt = attachments.constBegin(); atIt != attachments.constEnd(); ++atIt ) { icalcomponent_add_property( parent, writeAttachment( *atIt ) ); } // alarms Alarm::List::ConstIterator alarmIt; for ( alarmIt = incidence->alarms().constBegin(); alarmIt != incidence->alarms().constEnd(); ++alarmIt ) { if ( (*alarmIt)->enabled() ) { icalcomponent_add_component( parent, writeAlarm( *alarmIt ) ); } } // duration if ( incidence->hasDuration() ) { icaldurationtype duration; duration = writeICalDuration( incidence->duration() ); icalcomponent_add_property( parent, icalproperty_new_duration( duration ) ); } } //@cond PRIVATE void ICalFormatImpl::Private::writeIncidenceBase( icalcomponent *parent, IncidenceBase *incidenceBase ) { icalcomponent_add_property( parent, writeICalDateTimeProperty( ICAL_DTSTAMP_PROPERTY, KDateTime::currentUtcDateTime() ) ); // organizer stuff if ( !incidenceBase->organizer().isEmpty() ) { icalproperty *p = mImpl->writeOrganizer( incidenceBase->organizer() ); if ( p ) { icalcomponent_add_property( parent, p ); } } // attendees if ( incidenceBase->attendeeCount() > 0 ) { Attendee::List::ConstIterator it; for ( it = incidenceBase->attendees().constBegin(); it != incidenceBase->attendees().constEnd(); ++it ) { icalproperty *p = mImpl->writeAttendee( *it ); if ( p ) { icalcomponent_add_property( parent, p ); } } } // comments QStringList comments = incidenceBase->comments(); for ( QStringList::const_iterator it = comments.constBegin(); it != comments.constEnd(); ++it ) { icalcomponent_add_property( parent, icalproperty_new_comment( (*it).toUtf8() ) ); } // custom properties writeCustomProperties( parent, incidenceBase ); } void ICalFormatImpl::Private::writeCustomProperties( icalcomponent *parent, CustomProperties *properties ) { const QMap custom = properties->customProperties(); for ( QMap::ConstIterator c = custom.begin(); c != custom.end(); ++c ) { icalproperty *p = icalproperty_new_x( c.value().toUtf8() ); if ( !c.key().startsWith( "X-KDE" ) && //krazy:exclude=strings !c.key().startsWith( "X-LibKCal" ) && //krazy:exclude=strings !c.key().startsWith( "X-MICROSOFT" ) && //krazy:exclude=strings !c.key().startsWith( "X-MOZILLA" ) && //krazy:exclude=strings !c.key().startsWith( "X-PILOT" ) ) { //krazy:exclude=strings // use text values for the typical X-FOO property. // except for vendor specific X-FOO properties. icalvalue *text = icalvalue_new_text( c.value().toUtf8().data() ); icalproperty_set_value( p, text ); } icalproperty_set_x_name( p, c.key() ); icalcomponent_add_property( parent, p ); } } //@endcond icalproperty *ICalFormatImpl::writeOrganizer( const Person &organizer ) { if ( organizer.email().isEmpty() ) { return 0; } icalproperty *p = icalproperty_new_organizer( "MAILTO:" + organizer.email().toUtf8() ); if ( !organizer.name().isEmpty() ) { icalproperty_add_parameter( p, icalparameter_new_cn( quoteForParam( organizer.name() ).toUtf8() ) ); } // TODO: Write dir, sent-by and language return p; } icalproperty *ICalFormatImpl::writeDescription( const QString &description, bool isRich ) { icalproperty *p = icalproperty_new_description( description.toUtf8() ); if ( isRich ) { icalproperty_add_parameter( p, icalparameter_new_from_string( "X-KDE-TEXTFORMAT=HTML" ) ); } return p; } icalproperty *ICalFormatImpl::writeSummary( const QString &summary, bool isRich ) { icalproperty *p = icalproperty_new_summary( summary.toUtf8() ); if ( isRich ) { icalproperty_add_parameter( p, icalparameter_new_from_string( "X-KDE-TEXTFORMAT=HTML" ) ); } return p; } icalproperty *ICalFormatImpl::writeLocation( const QString &location, bool isRich ) { icalproperty *p = icalproperty_new_location( location.toUtf8() ); if ( isRich ) { icalproperty_add_parameter( p, icalparameter_new_from_string( "X-KDE-TEXTFORMAT=HTML" ) ); } return p; } icalproperty *ICalFormatImpl::writeAttendee( Attendee *attendee ) { if ( attendee->email().isEmpty() ) { return 0; } icalproperty *p = icalproperty_new_attendee( "mailto:" + attendee->email().toUtf8() ); if ( !attendee->name().isEmpty() ) { icalproperty_add_parameter( p, icalparameter_new_cn( quoteForParam( attendee->name() ).toUtf8() ) ); } icalproperty_add_parameter( p, icalparameter_new_rsvp( attendee->RSVP() ? ICAL_RSVP_TRUE : ICAL_RSVP_FALSE ) ); icalparameter_partstat status = ICAL_PARTSTAT_NEEDSACTION; switch ( attendee->status() ) { default: case Attendee::NeedsAction: status = ICAL_PARTSTAT_NEEDSACTION; break; case Attendee::Accepted: status = ICAL_PARTSTAT_ACCEPTED; break; case Attendee::Declined: status = ICAL_PARTSTAT_DECLINED; break; case Attendee::Tentative: status = ICAL_PARTSTAT_TENTATIVE; break; case Attendee::Delegated: status = ICAL_PARTSTAT_DELEGATED; break; case Attendee::Completed: status = ICAL_PARTSTAT_COMPLETED; break; case Attendee::InProcess: status = ICAL_PARTSTAT_INPROCESS; break; } icalproperty_add_parameter( p, icalparameter_new_partstat( status ) ); icalparameter_role role = ICAL_ROLE_REQPARTICIPANT; switch ( attendee->role() ) { case Attendee::Chair: role = ICAL_ROLE_CHAIR; break; default: case Attendee::ReqParticipant: role = ICAL_ROLE_REQPARTICIPANT; break; case Attendee::OptParticipant: role = ICAL_ROLE_OPTPARTICIPANT; break; case Attendee::NonParticipant: role = ICAL_ROLE_NONPARTICIPANT; break; } icalproperty_add_parameter( p, icalparameter_new_role( role ) ); if ( !attendee->uid().isEmpty() ) { icalparameter *icalparameter_uid = icalparameter_new_x( attendee->uid().toUtf8() ); icalparameter_set_xname( icalparameter_uid, "X-UID" ); icalproperty_add_parameter( p, icalparameter_uid ); } if ( !attendee->delegate().isEmpty() ) { icalparameter *icalparameter_delegate = icalparameter_new_delegatedto( attendee->delegate().toUtf8() ); icalproperty_add_parameter( p, icalparameter_delegate ); } if ( !attendee->delegator().isEmpty() ) { icalparameter *icalparameter_delegator = icalparameter_new_delegatedfrom( attendee->delegator().toUtf8() ); icalproperty_add_parameter( p, icalparameter_delegator ); } return p; } icalproperty *ICalFormatImpl::writeAttachment( Attachment *att ) { icalattach *attach; if ( att->isUri() ) { attach = icalattach_new_from_url( att->uri().toUtf8().data() ); } else { attach = icalattach_new_from_data ( ( unsigned char * )att->data(), 0, 0 ); } icalproperty *p = icalproperty_new_attach( attach ); if ( !att->mimeType().isEmpty() ) { icalproperty_add_parameter( p, icalparameter_new_fmttype( att->mimeType().toUtf8().data() ) ); } if ( att->isBinary() ) { icalproperty_add_parameter( p, icalparameter_new_value( ICAL_VALUE_BINARY ) ); icalproperty_add_parameter( p, icalparameter_new_encoding( ICAL_ENCODING_BASE64 ) ); } if ( att->showInline() ) { icalparameter *icalparameter_inline = icalparameter_new_x( "inline" ); icalparameter_set_xname( icalparameter_inline, "X-CONTENT-DISPOSITION" ); icalproperty_add_parameter( p, icalparameter_inline ); } if ( !att->label().isEmpty() ) { icalparameter *icalparameter_label = icalparameter_new_x( att->label().toUtf8() ); icalparameter_set_xname( icalparameter_label, "X-LABEL" ); icalproperty_add_parameter( p, icalparameter_label ); } if ( att->isLocal() ) { icalparameter *icalparameter_local = icalparameter_new_x( "local" ); icalparameter_set_xname( icalparameter_local, "X-KONTACT-TYPE" ); icalproperty_add_parameter( p, icalparameter_local ); } return p; } icalrecurrencetype ICalFormatImpl::writeRecurrenceRule( RecurrenceRule *recur ) { icalrecurrencetype r; icalrecurrencetype_clear( &r ); switch( recur->recurrenceType() ) { case RecurrenceRule::rSecondly: r.freq = ICAL_SECONDLY_RECURRENCE; break; case RecurrenceRule::rMinutely: r.freq = ICAL_MINUTELY_RECURRENCE; break; case RecurrenceRule::rHourly: r.freq = ICAL_HOURLY_RECURRENCE; break; case RecurrenceRule::rDaily: r.freq = ICAL_DAILY_RECURRENCE; break; case RecurrenceRule::rWeekly: r.freq = ICAL_WEEKLY_RECURRENCE; break; case RecurrenceRule::rMonthly: r.freq = ICAL_MONTHLY_RECURRENCE; break; case RecurrenceRule::rYearly: r.freq = ICAL_YEARLY_RECURRENCE; break; default: r.freq = ICAL_NO_RECURRENCE; kDebug() << "no recurrence"; break; } int index = 0; QList bys; QList::ConstIterator it; // Now write out the BY* parts: bys = recur->bySeconds(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_second[index++] = *it; } bys = recur->byMinutes(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_minute[index++] = *it; } bys = recur->byHours(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_hour[index++] = *it; } bys = recur->byMonthDays(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_month_day[index++] = icalrecurrencetype_day_position( (*it) * 8 ); } bys = recur->byYearDays(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_year_day[index++] = *it; } bys = recur->byWeekNumbers(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_week_no[index++] = *it; } bys = recur->byMonths(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_month[index++] = *it; } bys = recur->bySetPos(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_set_pos[index++] = *it; } QList byd = recur->byDays(); int day; index = 0; for ( QList::ConstIterator dit = byd.constBegin(); dit != byd.constEnd(); ++dit ) { day = (*dit).day() % 7 + 1; // convert from Monday=1 to Sunday=1 if ( (*dit).pos() < 0 ) { day += ( -(*dit).pos() ) * 8; day = -day; } else { day += (*dit).pos() * 8; } r.by_day[index++] = day; } r.week_start = static_cast( recur->weekStart() % 7 + 1 ); if ( recur->frequency() > 1 ) { // Dont' write out INTERVAL=1, because that's the default anyway r.interval = recur->frequency(); } if ( recur->duration() > 0 ) { r.count = recur->duration(); } else if ( recur->duration() == -1 ) { r.count = 0; } else { if ( recur->allDay() ) { r.until = writeICalDate( recur->endDt().date() ); } else { r.until = writeICalUtcDateTime( recur->endDt() ); } } return r; } icalcomponent *ICalFormatImpl::writeAlarm( Alarm *alarm ) { icalcomponent *a = icalcomponent_new( ICAL_VALARM_COMPONENT ); icalproperty_action action; icalattach *attach = 0; switch ( alarm->type() ) { case Alarm::Procedure: action = ICAL_ACTION_PROCEDURE; attach = icalattach_new_from_url( QFile::encodeName( alarm->programFile() ).data() ); icalcomponent_add_property( a, icalproperty_new_attach( attach ) ); if ( !alarm->programArguments().isEmpty() ) { icalcomponent_add_property( a, icalproperty_new_description( alarm->programArguments().toUtf8() ) ); } break; case Alarm::Audio: action = ICAL_ACTION_AUDIO; if ( !alarm->audioFile().isEmpty() ) { attach = icalattach_new_from_url( QFile::encodeName( alarm->audioFile() ).data() ); icalcomponent_add_property( a, icalproperty_new_attach( attach ) ); } break; case Alarm::Email: { action = ICAL_ACTION_EMAIL; const QList addresses = alarm->mailAddresses(); for ( QList::ConstIterator ad = addresses.constBegin(); ad != addresses.constEnd(); ++ad ) { if ( !(*ad).email().isEmpty() ) { icalproperty *p = icalproperty_new_attendee( "MAILTO:" + (*ad).email().toUtf8() ); if ( !(*ad).name().isEmpty() ) { icalproperty_add_parameter( p, icalparameter_new_cn( quoteForParam( (*ad).name() ).toUtf8() ) ); } icalcomponent_add_property( a, p ); } } icalcomponent_add_property( a, icalproperty_new_summary( alarm->mailSubject().toUtf8() ) ); icalcomponent_add_property( a, icalproperty_new_description( alarm->mailText().toUtf8() ) ); QStringList attachments = alarm->mailAttachments(); if ( attachments.count() > 0 ) { for ( QStringList::const_iterator at = attachments.constBegin(); at != attachments.constEnd(); ++at ) { attach = icalattach_new_from_url( QFile::encodeName( *at ).data() ); icalcomponent_add_property( a, icalproperty_new_attach( attach ) ); } } break; } case Alarm::Display: action = ICAL_ACTION_DISPLAY; icalcomponent_add_property( a, icalproperty_new_description( alarm->text().toUtf8() ) ); break; case Alarm::Invalid: default: kDebug() << "Unknown type of alarm"; action = ICAL_ACTION_NONE; break; } icalcomponent_add_property( a, icalproperty_new_action( action ) ); // Trigger time icaltriggertype trigger; if ( alarm->hasTime() ) { trigger.time = writeICalUtcDateTime( alarm->time() ); trigger.duration = icaldurationtype_null_duration(); } else { trigger.time = icaltime_null_time(); Duration offset; if ( alarm->hasStartOffset() ) { offset = alarm->startOffset(); } else { offset = alarm->endOffset(); } trigger.duration = writeICalDuration( offset ); } icalproperty *p = icalproperty_new_trigger( trigger ); if ( alarm->hasEndOffset() ) { icalproperty_add_parameter( p, icalparameter_new_related( ICAL_RELATED_END ) ); } icalcomponent_add_property( a, p ); // Repeat count and duration if ( alarm->repeatCount() ) { icalcomponent_add_property( a, icalproperty_new_repeat( alarm->repeatCount() ) ); icalcomponent_add_property( a, icalproperty_new_duration( writeICalDuration( alarm->snoozeTime() ) ) ); } // Custom properties const QMap custom = alarm->customProperties(); for ( QMap::ConstIterator c = custom.begin(); c != custom.end(); ++c ) { icalproperty *p = icalproperty_new_x( c.value().toUtf8() ); icalproperty_set_x_name( p, c.key() ); icalcomponent_add_property( a, p ); } return a; } Todo *ICalFormatImpl::readTodo( icalcomponent *vtodo, ICalTimeZones *tzlist ) { Todo *todo = new Todo; readIncidence( vtodo, todo, tzlist ); icalproperty *p = icalcomponent_get_first_property( vtodo, ICAL_ANY_PROPERTY ); - QStringList categories; - while ( p ) { icalproperty_kind kind = icalproperty_isa(p); switch ( kind ) { case ICAL_DUE_PROPERTY: { // due date/time KDateTime kdt = readICalDateTimeProperty( p, tzlist ); todo->setDtDue( kdt, true ); todo->setHasDueDate( true ); todo->setAllDay( kdt.isDateOnly() ); break; } case ICAL_COMPLETED_PROPERTY: // completion date/time todo->setCompleted( readICalDateTimeProperty( p, tzlist ) ); break; case ICAL_PERCENTCOMPLETE_PROPERTY: // Percent completed todo->setPercentComplete( icalproperty_get_percentcomplete( p ) ); break; case ICAL_RELATEDTO_PROPERTY: // related todo (parent) todo->setRelatedToUid( QString::fromUtf8( icalproperty_get_relatedto( p ) ) ); d->mTodosRelate.append( todo ); break; case ICAL_DTSTART_PROPERTY: // Flag that todo has start date. Value is read in by readIncidence(). if ( todo->comments().filter( "NoStartDate" ).count() ) { todo->setHasStartDate( false ); } else { todo->setHasStartDate( true ); } break; case ICAL_RECURRENCEID_PROPERTY: todo->setDtRecurrence( readICalDateTimeProperty( p, tzlist ) ); break; default: // TODO: do something about unknown properties? break; } p = icalcomponent_get_next_property( vtodo, ICAL_ANY_PROPERTY ); } if ( d->mCompat ) { d->mCompat->fixEmptySummary( todo ); } return todo; } Event *ICalFormatImpl::readEvent( icalcomponent *vevent, ICalTimeZones *tzlist ) { Event *event = new Event; readIncidence( vevent, event, tzlist ); icalproperty *p = icalcomponent_get_first_property( vevent, ICAL_ANY_PROPERTY ); - QStringList categories; - icalproperty_transp transparency; - bool dtEndProcessed = false; while ( p ) { icalproperty_kind kind = icalproperty_isa( p ); switch ( kind ) { case ICAL_DTEND_PROPERTY: { // end date and time KDateTime kdt = readICalDateTimeProperty( p, tzlist ); if ( kdt.isDateOnly() ) { // End date is non-inclusive QDate endDate = kdt.date().addDays( -1 ); if ( d->mCompat ) { d->mCompat->fixFloatingEnd( endDate ); } if ( endDate < event->dtStart().date() ) { endDate = event->dtStart().date(); } event->setDtEnd( KDateTime( endDate, event->dtStart().timeSpec() ) ); } else { event->setDtEnd( kdt ); event->setAllDay( false ); } dtEndProcessed = true; break; } case ICAL_RELATEDTO_PROPERTY: // related event (parent) event->setRelatedToUid( QString::fromUtf8( icalproperty_get_relatedto( p ) ) ); d->mEventsRelate.append( event ); break; case ICAL_TRANSP_PROPERTY: // Transparency - transparency = icalproperty_get_transp( p ); + { + icalproperty_transp transparency = icalproperty_get_transp( p ); if ( transparency == ICAL_TRANSP_TRANSPARENT ) { event->setTransparency( Event::Transparent ); } else { event->setTransparency( Event::Opaque ); } break; + } default: // TODO: do something about unknown properties? break; } p = icalcomponent_get_next_property( vevent, ICAL_ANY_PROPERTY ); } // according to rfc2445 the dtend shouldn't be written when it equals // start date. so assign one equal to start date. if ( !dtEndProcessed && !event->hasDuration() ) { event->setDtEnd( event->dtStart() ); event->setHasEndDate( false ); } QString msade = event->nonKDECustomProperty( "X-MICROSOFT-CDO-ALLDAYEVENT" ); if ( !msade.isEmpty() ) { bool allDay = ( msade == QLatin1String( "TRUE" ) ); event->setAllDay( allDay ); } if ( d->mCompat ) { d->mCompat->fixEmptySummary( event ); } return event; } FreeBusy *ICalFormatImpl::readFreeBusy( icalcomponent *vfreebusy ) { FreeBusy *freebusy = new FreeBusy; d->readIncidenceBase( vfreebusy, freebusy ); icalproperty *p = icalcomponent_get_first_property( vfreebusy, ICAL_ANY_PROPERTY ); FreeBusyPeriod::List periods; while ( p ) { icalproperty_kind kind = icalproperty_isa( p ); switch ( kind ) { case ICAL_DTSTART_PROPERTY: // start date and time (UTC) freebusy->setDtStart( readICalUtcDateTimeProperty( p ) ); break; case ICAL_DTEND_PROPERTY: // end Date and Time (UTC) freebusy->setDtEnd( readICalUtcDateTimeProperty( p ) ); break; case ICAL_FREEBUSY_PROPERTY: //Any FreeBusy Times (UTC) { icalperiodtype icalperiod = icalproperty_get_freebusy( p ); KDateTime period_start = readICalUtcDateTime( p, icalperiod.start ); FreeBusyPeriod period; if ( !icaltime_is_null_time( icalperiod.end ) ) { KDateTime period_end = readICalUtcDateTime( p, icalperiod.end ); period = FreeBusyPeriod( period_start, period_end ); } else { Duration duration ( readICalDuration( icalperiod.duration ) ); period = FreeBusyPeriod( period_start, duration ); } QByteArray param = icalproperty_get_parameter_as_string( p, "X-SUMMARY" ); period.setSummary( QString::fromUtf8( KCodecs::base64Decode( param ) ) ); param = icalproperty_get_parameter_as_string( p, "X-LOCATION" ); period.setLocation( QString::fromUtf8( KCodecs::base64Decode( param ) ) ); periods.append( period ); break; } default: // TODO: do something about unknown properties? break; } p = icalcomponent_get_next_property( vfreebusy, ICAL_ANY_PROPERTY ); } freebusy->addPeriods( periods ); return freebusy; } Journal *ICalFormatImpl::readJournal( icalcomponent *vjournal, ICalTimeZones *tzlist ) { Journal *journal = new Journal; readIncidence( vjournal, journal, tzlist ); return journal; } Attendee *ICalFormatImpl::readAttendee( icalproperty *attendee ) { // the following is a hack to support broken calendars (like WebCalendar 1.0.x) // that include non-RFC-compliant attendees. Otherwise libical 0.42 asserts. if ( !icalproperty_get_value( attendee ) ) { return 0; } icalparameter *p = 0; QString email = QString::fromUtf8( icalproperty_get_attendee( attendee ) ); if ( email.startsWith( QLatin1String( "mailto:" ), Qt::CaseInsensitive ) ) { email = email.mid( 7 ); } QString name; QString uid; p = icalproperty_get_first_parameter( attendee, ICAL_CN_PARAMETER ); if ( p ) { name = QString::fromUtf8( icalparameter_get_cn( p ) ); } else { } bool rsvp = false; p = icalproperty_get_first_parameter( attendee, ICAL_RSVP_PARAMETER ); if ( p ) { icalparameter_rsvp rsvpParameter = icalparameter_get_rsvp( p ); if ( rsvpParameter == ICAL_RSVP_TRUE ) { rsvp = true; } } Attendee::PartStat status = Attendee::NeedsAction; p = icalproperty_get_first_parameter( attendee, ICAL_PARTSTAT_PARAMETER ); if ( p ) { icalparameter_partstat partStatParameter = icalparameter_get_partstat( p ); switch( partStatParameter ) { default: case ICAL_PARTSTAT_NEEDSACTION: status = Attendee::NeedsAction; break; case ICAL_PARTSTAT_ACCEPTED: status = Attendee::Accepted; break; case ICAL_PARTSTAT_DECLINED: status = Attendee::Declined; break; case ICAL_PARTSTAT_TENTATIVE: status = Attendee::Tentative; break; case ICAL_PARTSTAT_DELEGATED: status = Attendee::Delegated; break; case ICAL_PARTSTAT_COMPLETED: status = Attendee::Completed; break; case ICAL_PARTSTAT_INPROCESS: status = Attendee::InProcess; break; } } Attendee::Role role = Attendee::ReqParticipant; p = icalproperty_get_first_parameter( attendee, ICAL_ROLE_PARAMETER ); if ( p ) { icalparameter_role roleParameter = icalparameter_get_role( p ); switch( roleParameter ) { case ICAL_ROLE_CHAIR: role = Attendee::Chair; break; default: case ICAL_ROLE_REQPARTICIPANT: role = Attendee::ReqParticipant; break; case ICAL_ROLE_OPTPARTICIPANT: role = Attendee::OptParticipant; break; case ICAL_ROLE_NONPARTICIPANT: role = Attendee::NonParticipant; break; } } p = icalproperty_get_first_parameter( attendee, ICAL_X_PARAMETER ); while ( p ) { QString xname = QString( icalparameter_get_xname( p ) ).toUpper(); QString xvalue = QString::fromUtf8( icalparameter_get_xvalue( p ) ); if ( xname == "X-UID" ) { uid = xvalue; } p = icalproperty_get_next_parameter( attendee, ICAL_X_PARAMETER ); } Attendee *a = new Attendee( name, email, rsvp, status, role, uid ); p = icalproperty_get_first_parameter( attendee, ICAL_DELEGATEDTO_PARAMETER ); if ( p ) { a->setDelegate( icalparameter_get_delegatedto( p ) ); } p = icalproperty_get_first_parameter( attendee, ICAL_DELEGATEDFROM_PARAMETER ); if ( p ) { a->setDelegator( icalparameter_get_delegatedfrom( p ) ); } return a; } Person ICalFormatImpl::readOrganizer( icalproperty *organizer ) { QString email = QString::fromUtf8( icalproperty_get_organizer( organizer ) ); if ( email.startsWith( QLatin1String( "mailto:" ), Qt::CaseInsensitive ) ) { email = email.mid( 7 ); } QString cn; icalparameter *p = icalproperty_get_first_parameter( organizer, ICAL_CN_PARAMETER ); if ( p ) { cn = QString::fromUtf8( icalparameter_get_cn( p ) ); } Person org( cn, email ); // TODO: Treat sent-by, dir and language here, too return org; } Attachment *ICalFormatImpl::readAttachment( icalproperty *attach ) { Attachment *attachment = 0; const char *p; icalvalue *value = icalproperty_get_value( attach ); switch( icalvalue_isa( value ) ) { case ICAL_ATTACH_VALUE: { icalattach *a = icalproperty_get_attach( attach ); if ( !icalattach_get_is_url( a ) ) { p = (const char *)icalattach_get_data( a ); if ( p ) { attachment = new Attachment( p ); } } else { p = icalattach_get_url( a ); if ( p ) { attachment = new Attachment( QString::fromUtf8( p ) ); } } break; } case ICAL_BINARY_VALUE: { icalattach *a = icalproperty_get_attach( attach ); p = (const char *)icalattach_get_data( a ); if ( p ) { attachment = new Attachment( p ); } break; } case ICAL_URI_VALUE: p = icalvalue_get_uri( value ); attachment = new Attachment( QString::fromUtf8( p ) ); break; default: break; } if ( attachment ) { icalparameter *p = icalproperty_get_first_parameter( attach, ICAL_FMTTYPE_PARAMETER ); if ( p ) { attachment->setMimeType( QString( icalparameter_get_fmttype( p ) ) ); } p = icalproperty_get_first_parameter( attach, ICAL_X_PARAMETER ); while ( p ) { QString xname = QString( icalparameter_get_xname( p ) ).toUpper(); QString xvalue = QString::fromUtf8( icalparameter_get_xvalue( p ) ); if ( xname == "X-CONTENT-DISPOSITION" ) { attachment->setShowInline( xvalue.toLower() == "inline" ); } if ( xname == "X-LABEL" ) { attachment->setLabel( xvalue ); } if ( xname == "X-KONTACT-TYPE" ) { attachment->setLocal( xvalue.toLower() == "local" ); } p = icalproperty_get_next_parameter( attach, ICAL_X_PARAMETER ); } p = icalproperty_get_first_parameter( attach, ICAL_X_PARAMETER ); while ( p ) { if ( strncmp ( icalparameter_get_xname( p ), "X-LABEL", 7 ) == 0 ) { attachment->setLabel( icalparameter_get_xvalue( p ) ); } p = icalproperty_get_next_parameter( attach, ICAL_X_PARAMETER ); } } return attachment; } void ICalFormatImpl::readIncidence( icalcomponent *parent, Incidence *incidence, ICalTimeZones *tzlist ) { d->readIncidenceBase( parent, incidence ); icalproperty *p = icalcomponent_get_first_property( parent, ICAL_ANY_PROPERTY ); const char *text; int intvalue, inttext; icaldurationtype icalduration; KDateTime kdt; QStringList categories; while ( p ) { icalproperty_kind kind = icalproperty_isa( p ); switch ( kind ) { case ICAL_CREATED_PROPERTY: incidence->setCreated( readICalDateTimeProperty( p, tzlist ) ); break; case ICAL_SEQUENCE_PROPERTY: // sequence intvalue = icalproperty_get_sequence( p ); incidence->setRevision( intvalue ); break; case ICAL_LASTMODIFIED_PROPERTY: // last modification UTC date/time incidence->setLastModified( readICalDateTimeProperty( p, tzlist ) ); break; case ICAL_DTSTART_PROPERTY: // start date and time kdt = readICalDateTimeProperty( p, tzlist ); incidence->setDtStart( kdt ); incidence->setAllDay( kdt.isDateOnly() ); break; case ICAL_DURATION_PROPERTY: // start date and time icalduration = icalproperty_get_duration( p ); incidence->setDuration( readICalDuration( icalduration ) ); break; case ICAL_DESCRIPTION_PROPERTY: // description { QString textStr = QString::fromUtf8( icalproperty_get_description( p ) ); if ( !textStr.isEmpty() ) { QString valStr = QString::fromUtf8( icalproperty_get_parameter_as_string( p, "X-KDE-TEXTFORMAT" ) ); if ( !valStr.compare( "HTML", Qt::CaseInsensitive ) ) { incidence->setDescription( textStr, true ); } else { incidence->setDescription( textStr, false ); } } } break; case ICAL_SUMMARY_PROPERTY: // summary { QString textStr = QString::fromUtf8( icalproperty_get_summary( p ) ); if ( !textStr.isEmpty() ) { QString valStr = QString::fromUtf8( icalproperty_get_parameter_as_string( p, "X-KDE-TEXTFORMAT" ) ); if ( !valStr.compare( "HTML", Qt::CaseInsensitive ) ) { incidence->setSummary( textStr, true ); } else { incidence->setSummary( textStr, false ); } } } break; case ICAL_LOCATION_PROPERTY: // location { if ( !icalproperty_get_value( p ) ) { //Fix for #191472. This is a pre-crash guard in case libical was //compiled in superstrict mode (--enable-icalerrors-are-fatal) //TODO: pre-crash guard other property getters too. break; } QString textStr = QString::fromUtf8( icalproperty_get_location( p ) ); if ( !textStr.isEmpty() ) { QString valStr = QString::fromUtf8( icalproperty_get_parameter_as_string( p, "X-KDE-TEXTFORMAT" ) ); if ( !valStr.compare( "HTML", Qt::CaseInsensitive ) ) { incidence->setLocation( textStr, true ); } else { incidence->setLocation( textStr, false ); } } } break; case ICAL_STATUS_PROPERTY: // status { Incidence::Status stat; switch ( icalproperty_get_status( p ) ) { case ICAL_STATUS_TENTATIVE: stat = Incidence::StatusTentative; break; case ICAL_STATUS_CONFIRMED: stat = Incidence::StatusConfirmed; break; case ICAL_STATUS_COMPLETED: stat = Incidence::StatusCompleted; break; case ICAL_STATUS_NEEDSACTION: stat = Incidence::StatusNeedsAction; break; case ICAL_STATUS_CANCELLED: stat = Incidence::StatusCanceled; break; case ICAL_STATUS_INPROCESS: stat = Incidence::StatusInProcess; break; case ICAL_STATUS_DRAFT: stat = Incidence::StatusDraft; break; case ICAL_STATUS_FINAL: stat = Incidence::StatusFinal; break; case ICAL_STATUS_X: incidence->setCustomStatus( QString::fromUtf8( icalvalue_get_x( icalproperty_get_value( p ) ) ) ); stat = Incidence::StatusX; break; case ICAL_STATUS_NONE: default: stat = Incidence::StatusNone; break; } if ( stat != Incidence::StatusX ) { incidence->setStatus( stat ); } break; } case ICAL_GEO_PROPERTY: // geo { icalgeotype geo = icalproperty_get_geo( p ); incidence->setGeoLatitude( geo.lat ); incidence->setGeoLongitude( geo.lon ); incidence->setHasGeo( true ); break; } case ICAL_PRIORITY_PROPERTY: // priority intvalue = icalproperty_get_priority( p ); if ( d->mCompat ) { intvalue = d->mCompat->fixPriority( intvalue ); } incidence->setPriority( intvalue ); break; case ICAL_CATEGORIES_PROPERTY: // categories + { + // We have always supported multiple CATEGORIES properties per component + // even though the RFC seems to indicate only 1 is permitted. + // We can't change that -- in order to retain backwards compatibility. text = icalproperty_get_categories( p ); - categories.append( QString::fromUtf8( text ) ); + const QString val = QString::fromUtf8( text ); + foreach ( const QString &cat, val.split( ',', QString::SkipEmptyParts ) ) { + // ensure no duplicates + if ( !categories.contains( cat ) ) { + categories.append( cat ); + } + } break; + } case ICAL_RRULE_PROPERTY: readRecurrenceRule( p, incidence ); break; case ICAL_RDATE_PROPERTY: kdt = readICalDateTimeProperty( p, tzlist ); if ( kdt.isValid() ) { if ( kdt.isDateOnly() ) { incidence->recurrence()->addRDate( kdt.date() ); } else { incidence->recurrence()->addRDateTime( kdt ); } } else { // TODO: RDates as period are not yet implemented! } break; case ICAL_EXRULE_PROPERTY: readExceptionRule( p, incidence ); break; case ICAL_EXDATE_PROPERTY: kdt = readICalDateTimeProperty( p, tzlist ); if ( kdt.isDateOnly() ) { incidence->recurrence()->addExDate( kdt.date() ); } else { incidence->recurrence()->addExDateTime( kdt ); } break; case ICAL_CLASS_PROPERTY: inttext = icalproperty_get_class( p ); if ( inttext == ICAL_CLASS_PUBLIC ) { incidence->setSecrecy( Incidence::SecrecyPublic ); } else if ( inttext == ICAL_CLASS_CONFIDENTIAL ) { incidence->setSecrecy( Incidence::SecrecyConfidential ); } else { incidence->setSecrecy( Incidence::SecrecyPrivate ); } break; case ICAL_ATTACH_PROPERTY: // attachments incidence->addAttachment( readAttachment( p ) ); break; default: // TODO: do something about unknown properties? break; } p = icalcomponent_get_next_property( parent, ICAL_ANY_PROPERTY ); } // Set the scheduling ID const QString uid = incidence->customProperty( "LIBKCAL", "ID" ); if ( !uid.isNull() ) { // The UID stored in incidencebase is actually the scheduling ID // It has to be stored in the iCal UID component for compatibility // with other iCal applications incidence->setSchedulingID( incidence->uid() ); incidence->setUid( uid ); } // Now that recurrence and exception stuff is completely set up, // do any backwards compatibility adjustments. if ( incidence->recurs() && d->mCompat ) { d->mCompat->fixRecurrence( incidence ); } // add categories incidence->setCategories( categories ); // iterate through all alarms for ( icalcomponent *alarm = icalcomponent_get_first_component( parent, ICAL_VALARM_COMPONENT ); alarm; alarm = icalcomponent_get_next_component( parent, ICAL_VALARM_COMPONENT ) ) { readAlarm( alarm, incidence, tzlist ); } // Fix incorrect alarm settings by other applications (like outloook 9) if ( d->mCompat ) { d->mCompat->fixAlarms( incidence ); } } //@cond PRIVATE void ICalFormatImpl::Private::readIncidenceBase( icalcomponent *parent, IncidenceBase *incidenceBase ) { icalproperty *p = icalcomponent_get_first_property( parent, ICAL_ANY_PROPERTY ); while ( p ) { icalproperty_kind kind = icalproperty_isa( p ); switch ( kind ) { case ICAL_UID_PROPERTY: // unique id incidenceBase->setUid( QString::fromUtf8( icalproperty_get_uid( p ) ) ); break; case ICAL_ORGANIZER_PROPERTY: // organizer incidenceBase->setOrganizer( mImpl->readOrganizer( p ) ); break; case ICAL_ATTENDEE_PROPERTY: // attendee incidenceBase->addAttendee( mImpl->readAttendee( p ) ); break; case ICAL_COMMENT_PROPERTY: incidenceBase->addComment( QString::fromUtf8( icalproperty_get_comment( p ) ) ); break; default: break; } p = icalcomponent_get_next_property( parent, ICAL_ANY_PROPERTY ); } // custom properties readCustomProperties( parent, incidenceBase ); } void ICalFormatImpl::Private::readCustomProperties( icalcomponent *parent, CustomProperties *properties ) { QMap customProperties; QString lastProperty; icalproperty *p = icalcomponent_get_first_property( parent, ICAL_X_PROPERTY ); while ( p ) { QString value = QString::fromUtf8( icalproperty_get_x( p ) ); const char *name = icalproperty_get_x_name( p ); if ( lastProperty != name ) { customProperties[name] = value; } else { customProperties[name] = customProperties[name].append( "," ).append( value ); } p = icalcomponent_get_next_property( parent, ICAL_X_PROPERTY ); lastProperty = name; } properties->setCustomProperties( customProperties ); } //@endcond void ICalFormatImpl::readRecurrenceRule( icalproperty *rrule, Incidence *incidence ) { Recurrence *recur = incidence->recurrence(); struct icalrecurrencetype r = icalproperty_get_rrule( rrule ); // dumpIcalRecurrence(r); RecurrenceRule *recurrule = new RecurrenceRule( /*incidence*/); recurrule->setStartDt( incidence->dtStart() ); readRecurrence( r, recurrule ); recur->addRRule( recurrule ); } void ICalFormatImpl::readExceptionRule( icalproperty *rrule, Incidence *incidence ) { struct icalrecurrencetype r = icalproperty_get_exrule( rrule ); // dumpIcalRecurrence(r); RecurrenceRule *recurrule = new RecurrenceRule( /*incidence*/); recurrule->setStartDt( incidence->dtStart() ); readRecurrence( r, recurrule ); Recurrence *recur = incidence->recurrence(); recur->addExRule( recurrule ); } void ICalFormatImpl::readRecurrence( const struct icalrecurrencetype &r, RecurrenceRule *recur ) { // Generate the RRULE string recur->setRRule( QString( icalrecurrencetype_as_string( const_cast( &r ) ) ) ); // Period switch ( r.freq ) { case ICAL_SECONDLY_RECURRENCE: recur->setRecurrenceType( RecurrenceRule::rSecondly ); break; case ICAL_MINUTELY_RECURRENCE: recur->setRecurrenceType( RecurrenceRule::rMinutely ); break; case ICAL_HOURLY_RECURRENCE: recur->setRecurrenceType( RecurrenceRule::rHourly ); break; case ICAL_DAILY_RECURRENCE: recur->setRecurrenceType( RecurrenceRule::rDaily ); break; case ICAL_WEEKLY_RECURRENCE: recur->setRecurrenceType( RecurrenceRule::rWeekly ); break; case ICAL_MONTHLY_RECURRENCE: recur->setRecurrenceType( RecurrenceRule::rMonthly ); break; case ICAL_YEARLY_RECURRENCE: recur->setRecurrenceType( RecurrenceRule::rYearly ); break; case ICAL_NO_RECURRENCE: default: recur->setRecurrenceType( RecurrenceRule::rNone ); } // Frequency recur->setFrequency( r.interval ); // Duration & End Date if ( !icaltime_is_null_time( r.until ) ) { icaltimetype t = r.until; recur->setEndDt( readICalUtcDateTime( 0, t ) ); } else { if ( r.count == 0 ) { recur->setDuration( -1 ); } else { recur->setDuration( r.count ); } } // Week start setting int wkst = ( r.week_start + 5 ) % 7 + 1; recur->setWeekStart( wkst ); // And now all BY* QList lst; int i; int index = 0; //@cond PRIVATE #define readSetByList( rrulecomp, setfunc ) \ index = 0; \ lst.clear(); \ while ( ( i = r.rrulecomp[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { \ lst.append( i ); \ } \ if ( !lst.isEmpty() ) { \ recur->setfunc( lst ); \ } //@endcond // BYSECOND, MINUTE and HOUR, MONTHDAY, YEARDAY, WEEKNUMBER, MONTH // and SETPOS are standard int lists, so we can treat them with the // same macro readSetByList( by_second, setBySeconds ); readSetByList( by_minute, setByMinutes ); readSetByList( by_hour, setByHours ); readSetByList( by_month_day, setByMonthDays ); readSetByList( by_year_day, setByYearDays ); readSetByList( by_week_no, setByWeekNumbers ); readSetByList( by_month, setByMonths ); readSetByList( by_set_pos, setBySetPos ); #undef readSetByList // BYDAY is a special case, since it's not an int list QList wdlst; short day; index=0; while ( ( day = r.by_day[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { RecurrenceRule::WDayPos pos; pos.setDay( ( icalrecurrencetype_day_day_of_week( day ) + 5 ) % 7 + 1 ); pos.setPos( icalrecurrencetype_day_position( day ) ); wdlst.append( pos ); } if ( !wdlst.isEmpty() ) { recur->setByDays( wdlst ); } // TODO: Store all X- fields of the RRULE inside the recurrence (so they are // preserved } void ICalFormatImpl::readAlarm( icalcomponent *alarm, Incidence *incidence, ICalTimeZones *tzlist ) { Alarm *ialarm = incidence->newAlarm(); ialarm->setRepeatCount( 0 ); ialarm->setEnabled( true ); // Determine the alarm's action type icalproperty *p = icalcomponent_get_first_property( alarm, ICAL_ACTION_PROPERTY ); Alarm::Type type = Alarm::Display; icalproperty_action action = ICAL_ACTION_DISPLAY; if ( !p ) { kDebug() << "Unknown type of alarm, using default"; // TODO: do something about unknown alarm type? } else { action = icalproperty_get_action( p ); switch ( action ) { case ICAL_ACTION_DISPLAY: type = Alarm::Display; break; case ICAL_ACTION_AUDIO: type = Alarm::Audio; break; case ICAL_ACTION_PROCEDURE: type = Alarm::Procedure; break; case ICAL_ACTION_EMAIL: type = Alarm::Email; break; default: break; // TODO: do something about invalid alarm type? } } ialarm->setType( type ); p = icalcomponent_get_first_property( alarm, ICAL_ANY_PROPERTY ); while ( p ) { icalproperty_kind kind = icalproperty_isa( p ); switch ( kind ) { case ICAL_TRIGGER_PROPERTY: { icaltriggertype trigger = icalproperty_get_trigger( p ); if ( !icaltime_is_null_time( trigger.time ) ) { //set the trigger to a specific time (which is not in rfc2445, btw) ialarm->setTime( readICalUtcDateTime( p, trigger.time, tzlist ) ); } else { //set the trigger to an offset from the incidence start or end time. if ( !icaldurationtype_is_bad_duration( trigger.duration ) ) { Duration duration( readICalDuration( trigger.duration ) ); icalparameter *param = icalproperty_get_first_parameter( p, ICAL_RELATED_PARAMETER ); if ( param && icalparameter_get_related( param ) == ICAL_RELATED_END ) { ialarm->setEndOffset( duration ); } else { ialarm->setStartOffset( duration ); } } else { // a bad duration was encountered, just set a 0 duration from start ialarm->setStartOffset( Duration( 0 ) ); } } break; } case ICAL_DURATION_PROPERTY: { icaldurationtype duration = icalproperty_get_duration( p ); ialarm->setSnoozeTime( readICalDuration( duration ) ); break; } case ICAL_REPEAT_PROPERTY: ialarm->setRepeatCount( icalproperty_get_repeat( p ) ); break; case ICAL_DESCRIPTION_PROPERTY: { // Only in DISPLAY and EMAIL and PROCEDURE alarms QString description = QString::fromUtf8( icalproperty_get_description( p ) ); switch ( action ) { case ICAL_ACTION_DISPLAY: ialarm->setText( description ); break; case ICAL_ACTION_PROCEDURE: ialarm->setProgramArguments( description ); break; case ICAL_ACTION_EMAIL: ialarm->setMailText( description ); break; default: break; } break; } case ICAL_SUMMARY_PROPERTY: // Only in EMAIL alarm ialarm->setMailSubject( QString::fromUtf8( icalproperty_get_summary( p ) ) ); break; case ICAL_ATTENDEE_PROPERTY: { // Only in EMAIL alarm QString email = QString::fromUtf8( icalproperty_get_attendee( p ) ); if ( email.startsWith( QLatin1String( "mailto:" ), Qt::CaseInsensitive ) ) { email = email.mid( 7 ); } QString name; icalparameter *param = icalproperty_get_first_parameter( p, ICAL_CN_PARAMETER ); if ( param ) { name = QString::fromUtf8( icalparameter_get_cn( param ) ); } ialarm->addMailAddress( Person( name, email ) ); break; } case ICAL_ATTACH_PROPERTY: { // Only in AUDIO and EMAIL and PROCEDURE alarms Attachment *attach = readAttachment( p ); if ( attach && attach->isUri() ) { switch ( action ) { case ICAL_ACTION_AUDIO: ialarm->setAudioFile( attach->uri() ); break; case ICAL_ACTION_PROCEDURE: ialarm->setProgramFile( attach->uri() ); break; case ICAL_ACTION_EMAIL: ialarm->addMailAttachment( attach->uri() ); break; default: break; } } else { kDebug() << "Alarm attachments currently only support URIs," << "but no binary data"; } delete attach; break; } default: break; } p = icalcomponent_get_next_property( alarm, ICAL_ANY_PROPERTY ); } // custom properties d->readCustomProperties( alarm, ialarm ); // TODO: check for consistency of alarm properties } icaldatetimeperiodtype ICalFormatImpl::writeICalDatePeriod( const QDate &date ) { icaldatetimeperiodtype t; t.time = writeICalDate( date ); t.period = icalperiodtype_null_period(); return t; } icaltimetype ICalFormatImpl::writeICalDate( const QDate &date ) { icaltimetype t = icaltime_null_time(); t.year = date.year(); t.month = date.month(); t.day = date.day(); t.hour = 0; t.minute = 0; t.second = 0; t.is_date = 1; t.is_utc = 0; t.zone = 0; return t; } icaltimetype ICalFormatImpl::writeICalDateTime( const KDateTime &datetime ) { icaltimetype t = icaltime_null_time(); t.year = datetime.date().year(); t.month = datetime.date().month(); t.day = datetime.date().day(); t.hour = datetime.time().hour(); t.minute = datetime.time().minute(); t.second = datetime.time().second(); t.is_date = 0; t.zone = 0; // zone is NOT set t.is_utc = datetime.isUtc() ? 1 : 0; // _dumpIcaltime( t ); return t; } icalproperty *ICalFormatImpl::writeICalDateTimeProperty( const icalproperty_kind type, const KDateTime &dt, ICalTimeZones *tzlist, ICalTimeZones *tzUsedList ) { icaltimetype t; switch ( type ) { case ICAL_DTSTAMP_PROPERTY: case ICAL_CREATED_PROPERTY: case ICAL_LASTMODIFIED_PROPERTY: t = writeICalDateTime( dt.toUtc() ); break; default: t = writeICalDateTime( dt ); break; } icalproperty *p; switch ( type ) { case ICAL_DTSTAMP_PROPERTY: p = icalproperty_new_dtstamp( t ); break; case ICAL_CREATED_PROPERTY: p = icalproperty_new_created( t ); break; case ICAL_LASTMODIFIED_PROPERTY: p = icalproperty_new_lastmodified( t ); break; case ICAL_DTSTART_PROPERTY: // start date and time p = icalproperty_new_dtstart( t ); break; case ICAL_DTEND_PROPERTY: // end date and time p = icalproperty_new_dtend( t ); break; case ICAL_DUE_PROPERTY: p = icalproperty_new_due( t ); break; case ICAL_RECURRENCEID_PROPERTY: p = icalproperty_new_recurrenceid( t ); break; case ICAL_EXDATE_PROPERTY: p = icalproperty_new_exdate( t ); break; default: { icaldatetimeperiodtype tp; tp.time = t; tp.period = icalperiodtype_null_period(); switch ( type ) { case ICAL_RDATE_PROPERTY: p = icalproperty_new_rdate( tp ); break; default: return 0; } } } KTimeZone ktz; if ( !t.is_utc ) { ktz = dt.timeZone(); } if ( ktz.isValid() ) { if ( tzlist ) { ICalTimeZone tz = tzlist->zone( ktz.name() ); if ( !tz.isValid() ) { // The time zone isn't in the list of known zones for the calendar // - add it to the calendar's zone list ICalTimeZone tznew( ktz ); tzlist->add( tznew ); tz = tznew; } if ( tzUsedList ) { tzUsedList->add( tz ); } } icalproperty_add_parameter( p, icalparameter_new_tzid( ktz.name().toUtf8() ) ); } return p; } KDateTime ICalFormatImpl::readICalDateTime( icalproperty *p, const icaltimetype &t, ICalTimeZones *tzlist, bool utc ) { // kDebug(); // _dumpIcaltime( t ); KDateTime::Spec timeSpec; if ( t.is_utc || t.zone == icaltimezone_get_utc_timezone() ) { timeSpec = KDateTime::UTC; // the time zone is UTC utc = false; // no need to convert to UTC } else { if ( !tzlist ) { utc = true; // should be UTC, but it isn't } icalparameter *param = p ? icalproperty_get_first_parameter( p, ICAL_TZID_PARAMETER ) : 0; const char *tzid = param ? icalparameter_get_tzid( param ) : 0; if ( !tzid ) { timeSpec = KDateTime::ClockTime; } else { QString tzidStr = QString::fromUtf8( tzid ); ICalTimeZone tz; if ( tzlist ) { tz = tzlist->zone( tzidStr ); } if ( !tz.isValid() ) { // The time zone is not in the existing list for the calendar. // Try to read it from the system or libical databases. ICalTimeZoneSource tzsource; ICalTimeZone newtz = tzsource.standardZone( tzidStr ); if ( newtz.isValid() && tzlist ) { tzlist->add( newtz ); } tz = newtz; } timeSpec = tz.isValid() ? KDateTime::Spec( tz ) : KDateTime::LocalZone; } } KDateTime result( QDate( t.year, t.month, t.day ), QTime( t.hour, t.minute, t.second ), timeSpec ); return utc ? result.toUtc() : result; } QDate ICalFormatImpl::readICalDate( icaltimetype t ) { return QDate( t.year, t.month, t.day ); } KDateTime ICalFormatImpl::readICalDateTimeProperty( icalproperty *p, ICalTimeZones *tzlist, bool utc ) { icaldatetimeperiodtype tp; icalproperty_kind kind = icalproperty_isa( p ); switch ( kind ) { case ICAL_CREATED_PROPERTY: // UTC date/time tp.time = icalproperty_get_created( p ); utc = true; break; case ICAL_LASTMODIFIED_PROPERTY: // last modification UTC date/time tp.time = icalproperty_get_lastmodified( p ); utc = true; break; case ICAL_DTSTART_PROPERTY: // start date and time (UTC for freebusy) tp.time = icalproperty_get_dtstart( p ); break; case ICAL_DTEND_PROPERTY: // end date and time (UTC for freebusy) tp.time = icalproperty_get_dtend( p ); break; case ICAL_DUE_PROPERTY: // due date/time tp.time = icalproperty_get_due( p ); break; case ICAL_COMPLETED_PROPERTY: // UTC completion date/time tp.time = icalproperty_get_completed( p ); utc = true; break; case ICAL_RECURRENCEID_PROPERTY: tp.time = icalproperty_get_recurrenceid( p ); break; case ICAL_EXDATE_PROPERTY: tp.time = icalproperty_get_exdate( p ); break; default: switch ( kind ) { case ICAL_RDATE_PROPERTY: tp = icalproperty_get_rdate( p ); break; default: return KDateTime(); } if ( !icaltime_is_valid_time( tp.time ) ) { return KDateTime(); // a time period was found (not implemented yet) } break; } if ( tp.time.is_date ) { return KDateTime( readICalDate( tp.time ), KDateTime::Spec::ClockTime() ); } else { return readICalDateTime( p, tp.time, tzlist, utc ); } } icaldurationtype ICalFormatImpl::writeICalDuration( const Duration &duration ) { // should be able to use icaldurationtype_from_int(), except we know // that some older tools do not properly support weeks. So we never // set a week duration, only days icaldurationtype d; int value = duration.value(); d.is_neg = ( value < 0 ) ? 1 : 0; if ( value < 0 ) { value = -value; } if ( duration.isDaily() ) { d.weeks = 0; d.days = value; d.hours = d.minutes = d.seconds = 0; } else { d.weeks = 0; d.days = value / gSecondsPerDay; value %= gSecondsPerDay; d.hours = value / gSecondsPerHour; value %= gSecondsPerHour; d.minutes = value / gSecondsPerMinute; value %= gSecondsPerMinute; d.seconds = value; } return d; } Duration ICalFormatImpl::readICalDuration( icaldurationtype d ) { int days = d.weeks * 7; days += d.days; int seconds = d.hours * gSecondsPerHour; seconds += d.minutes * gSecondsPerMinute; seconds += d.seconds; if ( seconds ) { seconds += days * gSecondsPerDay; if ( d.is_neg ) { seconds = -seconds; } return Duration( seconds, Duration::Seconds ); } else { if ( d.is_neg ) { days = -days; } return Duration( days, Duration::Days ); } } icalcomponent *ICalFormatImpl::createCalendarComponent( Calendar *cal ) { icalcomponent *calendar; // Root component calendar = icalcomponent_new( ICAL_VCALENDAR_COMPONENT ); icalproperty *p; // Product Identifier p = icalproperty_new_prodid( CalFormat::productId().toUtf8() ); icalcomponent_add_property( calendar, p ); // iCalendar version (2.0) p = icalproperty_new_version( const_cast(_ICAL_VERSION) ); icalcomponent_add_property( calendar, p ); // Add time zone if ( cal && cal->timeZones() ) { const ICalTimeZones::ZoneMap zmaps = cal->timeZones()->zones(); for ( ICalTimeZones::ZoneMap::ConstIterator it=zmaps.constBegin(); it != zmaps.constEnd(); ++it ) { icaltimezone *icaltz = (*it).icalTimezone(); if ( !icaltz ) { kError() << "bad time zone"; } else { icalcomponent *tz = icalcomponent_new_clone( icaltimezone_get_component( icaltz ) ); icalcomponent_add_component( calendar, tz ); icaltimezone_free( icaltz, 1 ); } } } // Custom properties if( cal != 0 ) { d->writeCustomProperties( calendar, cal ); } return calendar; } // take a raw vcalendar (i.e. from a file on disk, clipboard, etc. etc. // and break it down from its tree-like format into the dictionary format // that is used internally in the ICalFormatImpl. bool ICalFormatImpl::populate( Calendar *cal, icalcomponent *calendar ) { // this function will populate the caldict dictionary and other event // lists. It turns vevents into Events and then inserts them. if ( !calendar ) { return false; } // TODO: check for METHOD icalproperty *p; p = icalcomponent_get_first_property( calendar, ICAL_PRODID_PROPERTY ); if ( !p ) { kDebug() << "No PRODID property found"; d->mLoadedProductId = ""; } else { d->mLoadedProductId = QString::fromUtf8( icalproperty_get_prodid( p ) ); delete d->mCompat; d->mCompat = CompatFactory::createCompat( d->mLoadedProductId ); } p = icalcomponent_get_first_property( calendar, ICAL_VERSION_PROPERTY ); if ( !p ) { kDebug() << "No VERSION property found"; d->mParent->setException( new ErrorFormat( ErrorFormat::CalVersionUnknown ) ); return false; } else { const char *version = icalproperty_get_version( p ); - + if ( !version ) { + kDebug() << "No VERSION property found"; + d->mParent->setException( new ErrorFormat( + ErrorFormat::CalVersionUnknown, + i18n( "No VERSION property found" ) ) ); + return false; + } if ( strcmp( version, "1.0" ) == 0 ) { kDebug() << "Expected iCalendar, got vCalendar"; - d->mParent->setException( - new ErrorFormat( ErrorFormat::CalVersion1, - i18n( "Expected iCalendar format" ) ) ); + d->mParent->setException( new ErrorFormat( + ErrorFormat::CalVersion1, + i18n( "Expected iCalendar, got vCalendar format" ) ) ); return false; } else if ( strcmp( version, "2.0" ) != 0 ) { kDebug() << "Expected iCalendar, got unknown format"; - d->mParent->setException( new ErrorFormat( ErrorFormat::CalVersionUnknown ) ); + d->mParent->setException( new ErrorFormat( + ErrorFormat::CalVersionUnknown, + i18n( "Expected iCalendar, got unknown format" ) ) ); return false; } } // Populate the calendar's time zone collection with all VTIMEZONE components ICalTimeZones *tzlist = cal->timeZones(); ICalTimeZoneSource tzs; tzs.parse( calendar, *tzlist ); // custom properties d->readCustomProperties( calendar, cal ); // Store all events with a relatedTo property in a list for post-processing d->mEventsRelate.clear(); d->mTodosRelate.clear(); // TODO: make sure that only actually added events go to this lists. icalcomponent *c; // Iterate through all todos c = icalcomponent_get_first_component( calendar, ICAL_VTODO_COMPONENT ); while ( c ) { Todo *todo = readTodo( c, tzlist ); if ( todo ) { Todo *old = cal->todo( todo->uid() ); if ( old ) { cal->deleteTodo( old ); d->mTodosRelate.removeAll( old ); } cal->addTodo( todo ); } c = icalcomponent_get_next_component( calendar, ICAL_VTODO_COMPONENT ); } // Iterate through all events c = icalcomponent_get_first_component( calendar, ICAL_VEVENT_COMPONENT ); while ( c ) { Event *event = readEvent( c, tzlist ); if ( event ) { Event *old = cal->event( event->uid() ); if ( old ) { cal->deleteEvent( old ); d->mEventsRelate.removeAll( old ); } cal->addEvent( event ); } c = icalcomponent_get_next_component( calendar, ICAL_VEVENT_COMPONENT ); } // Iterate through all journals c = icalcomponent_get_first_component( calendar, ICAL_VJOURNAL_COMPONENT ); while ( c ) { Journal *journal = readJournal( c, tzlist ); if ( journal ) { Journal *old = cal->journal( journal->uid() ); if ( old ) { cal->deleteJournal( old ); } cal->addJournal( journal ); } c = icalcomponent_get_next_component( calendar, ICAL_VJOURNAL_COMPONENT ); } // Post-Process list of events with relations, put Event objects in relation Event::List::ConstIterator eIt; for ( eIt = d->mEventsRelate.constBegin(); eIt != d->mEventsRelate.constEnd(); ++eIt ) { (*eIt)->setRelatedTo( cal->incidence( (*eIt)->relatedToUid() ) ); } Todo::List::ConstIterator tIt; for ( tIt = d->mTodosRelate.constBegin(); tIt != d->mTodosRelate.constEnd(); ++tIt ) { (*tIt)->setRelatedTo( cal->incidence( (*tIt)->relatedToUid() ) ); } // TODO: Remove any previous time zones no longer referenced in the calendar return true; } QString ICalFormatImpl::extractErrorProperty( icalcomponent *c ) { QString errorMessage; icalproperty *error; error = icalcomponent_get_first_property( c, ICAL_XLICERROR_PROPERTY ); while ( error ) { errorMessage += icalproperty_get_xlicerror( error ); errorMessage += '\n'; error = icalcomponent_get_next_property( c, ICAL_XLICERROR_PROPERTY ); } return errorMessage; } void ICalFormatImpl::dumpIcalRecurrence( icalrecurrencetype r ) { int i; kDebug() << " Freq:" << r.freq; kDebug() << " Until:" << icaltime_as_ical_string( r.until ); kDebug() << " Count:" << r.count; if ( r.by_day[0] != ICAL_RECURRENCE_ARRAY_MAX ) { int index = 0; QString out = " By Day: "; while ( ( i = r.by_day[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { out.append( QString::number( i ) + ' ' ); } kDebug() << out; } if ( r.by_month_day[0] != ICAL_RECURRENCE_ARRAY_MAX ) { int index = 0; QString out = " By Month Day: "; while ( ( i = r.by_month_day[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { out.append( QString::number( i ) + ' ' ); } kDebug() << out; } if ( r.by_year_day[0] != ICAL_RECURRENCE_ARRAY_MAX ) { int index = 0; QString out = " By Year Day: "; while ( ( i = r.by_year_day[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { out.append( QString::number( i ) + ' ' ); } kDebug() << out; } if ( r.by_month[0] != ICAL_RECURRENCE_ARRAY_MAX ) { int index = 0; QString out = " By Month: "; while ( ( i = r.by_month[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { out.append( QString::number( i ) + ' ' ); } kDebug() << out; } if ( r.by_set_pos[0] != ICAL_RECURRENCE_ARRAY_MAX ) { int index = 0; QString out = " By Set Pos: "; while ( ( i = r.by_set_pos[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { kDebug() << "=========" << i; out.append( QString::number( i ) + ' ' ); } kDebug() << out; } } icalcomponent *ICalFormatImpl::createScheduleComponent( IncidenceBase *incidence, iTIPMethod method ) { icalcomponent *message = createCalendarComponent(); // Create VTIMEZONE components for this incidence ICalTimeZones zones; if ( incidence ) { if ( incidence->type() == "Event" ) { Event *ev = static_cast( incidence ); if ( ev ) { if ( ev->dtStart().isValid() ) { zones.add( ICalTimeZone( ev->dtStart().timeZone() ) ); } if ( ev->hasEndDate() && ev->dtEnd().isValid() ) { zones.add( ICalTimeZone( ev->dtEnd().timeZone() ) ); } } } else if ( incidence->type() == "Todo" ) { Todo *t = static_cast( incidence ); if ( t ) { if ( t->hasStartDate() && t->dtStart().isValid() ) { zones.add( ICalTimeZone( t->dtStart( true ).timeZone() ) ); } if ( t->hasDueDate() && t->dtDue().isValid() ) { zones.add( ICalTimeZone( t->dtDue().timeZone() ) ); } } } else if ( incidence->type() == "Journal" ) { Journal *j = static_cast( incidence ); if ( j ) { if ( j->dtStart().isValid() ) { zones.add( ICalTimeZone( j->dtStart().timeZone() ) ); } } } const ICalTimeZones::ZoneMap zmaps = zones.zones(); for ( ICalTimeZones::ZoneMap::ConstIterator it=zmaps.constBegin(); it != zmaps.constEnd(); ++it ) { icaltimezone *icaltz = (*it).icalTimezone(); if ( !icaltz ) { kError() << "bad time zone"; } else { icalcomponent *tz = icalcomponent_new_clone( icaltimezone_get_component( icaltz ) ); icalcomponent_add_component( message, tz ); icaltimezone_free( icaltz, 1 ); } } } icalproperty_method icalmethod = ICAL_METHOD_NONE; switch (method) { case iTIPPublish: icalmethod = ICAL_METHOD_PUBLISH; break; case iTIPRequest: icalmethod = ICAL_METHOD_REQUEST; break; case iTIPRefresh: icalmethod = ICAL_METHOD_REFRESH; break; case iTIPCancel: icalmethod = ICAL_METHOD_CANCEL; break; case iTIPAdd: icalmethod = ICAL_METHOD_ADD; break; case iTIPReply: icalmethod = ICAL_METHOD_REPLY; break; case iTIPCounter: icalmethod = ICAL_METHOD_COUNTER; break; case iTIPDeclineCounter: icalmethod = ICAL_METHOD_DECLINECOUNTER; break; default: kDebug() << "Unknown method"; return message; } icalcomponent_add_property( message, icalproperty_new_method( icalmethod ) ); icalcomponent *inc = writeIncidence( incidence, method ); /* * RFC 2446 states in section 3.4.3 ( REPLY to a VTODO ), that * a REQUEST-STATUS property has to be present. For the other two, event and * free busy, it can be there, but is optional. Until we do more * fine grained handling, assume all is well. Note that this is the * status of the _request_, not the attendee. Just to avoid confusion. * - till */ if ( icalmethod == ICAL_METHOD_REPLY ) { struct icalreqstattype rst; rst.code = ICAL_2_0_SUCCESS_STATUS; rst.desc = 0; rst.debug = 0; icalcomponent_add_property( inc, icalproperty_new_requeststatus( rst ) ); } icalcomponent_add_component( message, inc ); return message; } diff --git a/kcal/incidenceformatter.cpp b/kcal/incidenceformatter.cpp index ef75db60c..36998b0a1 100644 --- a/kcal/incidenceformatter.cpp +++ b/kcal/incidenceformatter.cpp @@ -1,2428 +1,2741 @@ /* This file is part of the kcal library. Copyright (c) 2001 Cornelius Schumacher Copyright (c) 2004 Reinhold Kainhofer Copyright (c) 2005 Rafal Rzepecki + Copyright (c) 2009 Klarlvdalens Datakonsult AB, a KDAB Group company This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** @file This file is part of the API for handling calendar data and provides static functions for formatting Incidences for various purposes. @brief Provides methods to format Incidences in various ways for display purposes. @author Cornelius Schumacher \ @author Reinhold Kainhofer \ */ #include "incidenceformatter.h" #include "attachment.h" #include "event.h" #include "todo.h" #include "journal.h" #include "calendar.h" #include "calendarlocal.h" #include "icalformat.h" #include "freebusy.h" #include "calendarresources.h" #include "kpimutils/email.h" #include "kabc/phonenumber.h" #include "kabc/vcardconverter.h" #include "kabc/stdaddressbook.h" #include +#include +#include #include #include #include #include #include #include #include #include #include using namespace KCal; /******************************************************************* * Helper functions for the extensive display (event viewer) *******************************************************************/ //@cond PRIVATE static QString eventViewerAddLink( const QString &ref, const QString &text, bool newline = true ) { QString tmpStr( "" + text + "" ); if ( newline ) { tmpStr += '\n'; } return tmpStr; } static QString eventViewerAddTag( const QString &tag, const QString &text ) { int numLineBreaks = text.count( "\n" ); QString str = '<' + tag + '>'; QString tmpText = text; QString tmpStr = str; if( numLineBreaks >= 0 ) { if ( numLineBreaks > 0 ) { int pos = 0; QString tmp; for ( int i = 0; i <= numLineBreaks; ++i ) { pos = tmpText.indexOf( "\n" ); tmp = tmpText.left( pos ); tmpText = tmpText.right( tmpText.length() - pos - 1 ); tmpStr += tmp + "
"; } } else { tmpStr += tmpText; } } tmpStr += "'; return tmpStr; } static QString eventViewerFormatCategories( Incidence *event ) { QString tmpStr; if ( !event->categoriesStr().isEmpty() ) { if ( event->categories().count() == 1 ) { tmpStr = eventViewerAddTag( "h3", i18n( "Category" ) ); } else { tmpStr = eventViewerAddTag( "h3", i18n( "Categories" ) ); } tmpStr += eventViewerAddTag( "p", event->categoriesStr() ); } return tmpStr; } static QString linkPerson( const QString &email, QString name, QString uid, const QString &iconPath ) { // Make the search, if there is an email address to search on, // and either name or uid is missing if ( !email.isEmpty() && ( name.isEmpty() || uid.isEmpty() ) ) { KABC::AddressBook *add_book = KABC::StdAddressBook::self( true ); KABC::Addressee::List addressList = add_book->findByEmail( email ); KABC::Addressee o = ( !addressList.isEmpty() ? addressList.first() : KABC::Addressee() ); if ( !o.isEmpty() && addressList.size() < 2 ) { if ( name.isEmpty() ) { // No name set, so use the one from the addressbook name = o.formattedName(); } uid = o.uid(); } else { // Email not found in the addressbook. Don't make a link uid.clear(); } } // Show the attendee QString tmpString = "
  • "; if ( !uid.isEmpty() ) { // There is a UID, so make a link to the addressbook if ( name.isEmpty() ) { // Use the email address for text tmpString += eventViewerAddLink( "uid:" + uid, email ); } else { tmpString += eventViewerAddLink( "uid:" + uid, name ); } } else { // No UID, just show some text tmpString += ( name.isEmpty() ? email : name ); } tmpString += '\n'; // Make the mailto link if ( !email.isEmpty() && !iconPath.isNull() ) { KUrl mailto; mailto.setProtocol( "mailto" ); mailto.setPath( email ); tmpString += eventViewerAddLink( mailto.url(), "" ); } tmpString += "
  • \n"; return tmpString; } static QString eventViewerFormatAttendees( Incidence *event ) { QString tmpStr; Attendee::List attendees = event->attendees(); if ( attendees.count() ) { KIconLoader *iconLoader = KIconLoader::global(); const QString iconPath = iconLoader->iconPath( "mail-message-new", KIconLoader::Small ); // Add organizer link tmpStr += eventViewerAddTag( "h4", i18n( "Organizer" ) ); tmpStr += "
      "; tmpStr += linkPerson( event->organizer().email(), event->organizer().name(), QString(), iconPath ); tmpStr += "
    "; // Add attendees links tmpStr += eventViewerAddTag( "h4", i18n( "Attendees" ) ); tmpStr += "
      "; Attendee::List::ConstIterator it; for ( it = attendees.constBegin(); it != attendees.constEnd(); ++it ) { Attendee *a = *it; tmpStr += linkPerson( a->email(), a->name(), a->uid(), iconPath ); if ( !a->delegator().isEmpty() ) { tmpStr += i18n( " (delegated by %1)", a->delegator() ); } if ( !a->delegate().isEmpty() ) { tmpStr += i18n( " (delegated to %1)", a->delegate() ); } } tmpStr += "
    "; } return tmpStr; } static QString eventViewerFormatAttachments( Incidence *i ) { QString tmpStr; Attachment::List as = i->attachments(); if ( as.count() > 0 ) { Attachment::List::ConstIterator it; for ( it = as.constBegin(); it != as.constEnd(); ++it ) { if ( (*it)->isUri() ) { tmpStr += eventViewerAddLink( (*it)->uri(), (*it)->label() ); tmpStr += "
    "; } } } return tmpStr; } /* FIXME:This function depends of kaddressbook. Is necessary a new type of event? */ static QString eventViewerFormatBirthday( Event *event ) { if ( !event ) { return QString(); } if ( event->customProperty( "KABC", "BIRTHDAY" ) != "YES" ) { return QString(); } QString uid_1 = event->customProperty( "KABC", "UID-1" ); QString name_1 = event->customProperty( "KABC", "NAME-1" ); QString email_1= event->customProperty( "KABC", "EMAIL-1" ); KIconLoader *iconLoader = KIconLoader::global(); const QString iconPath = iconLoader->iconPath( "mail-message-new", KIconLoader::Small ); //TODO: add a tart icon QString tmpString = "
      "; tmpString += linkPerson( email_1, name_1, uid_1, iconPath ); if ( event->customProperty( "KABC", "ANNIVERSARY" ) == "YES" ) { QString uid_2 = event->customProperty( "KABC", "UID-2" ); QString name_2 = event->customProperty( "KABC", "NAME-2" ); QString email_2= event->customProperty( "KABC", "EMAIL-2" ); tmpString += linkPerson( email_2, name_2, uid_2, iconPath ); } tmpString += "
    "; return tmpString; } static QString eventViewerFormatHeader( Incidence *incidence ) { QString tmpStr = ""; // show icons KIconLoader *iconLoader = KIconLoader::global(); tmpStr += ""; tmpStr += ""; tmpStr += "
    "; // TODO: KDE5. Make the function QString Incidence::getPixmap() so we don't // need downcasting. if ( incidence->type() == "Todo" ) { tmpStr += "( incidence ); if ( !todo->isCompleted() ) { tmpStr += iconLoader->iconPath( "view-calendar-tasks", KIconLoader::Small ); } else { tmpStr += iconLoader->iconPath( "task-complete", KIconLoader::Small ); } tmpStr += "\">"; } if ( incidence->type() == "Event" ) { tmpStr += "iconPath( "view-calendar-day", KIconLoader::Small ) + "\">"; } if ( incidence->type() == "Journal" ) { tmpStr += "iconPath( "view-pim-journal", KIconLoader::Small ) + "\">"; } if ( incidence->isAlarmEnabled() ) { tmpStr += "iconPath( "preferences-desktop-notification-bell", KIconLoader::Small ) + "\">"; } if ( incidence->recurs() ) { tmpStr += "iconPath( "edit-redo", KIconLoader::Small ) + "\">"; } if ( incidence->isReadOnly() ) { tmpStr += "iconPath( "object-locked", KIconLoader::Small ) + "\">"; } tmpStr += "" + eventViewerAddTag( "h2", incidence->richSummary() ) + "
    "; return tmpStr; } static QString eventViewerFormatEvent( Event *event, KDateTime::Spec spec ) { if ( !event ) { return QString(); } QString tmpStr = eventViewerFormatHeader( event ); tmpStr += ""; if ( !event->location().isEmpty() ) { tmpStr += ""; tmpStr += ""; tmpStr += ""; tmpStr += ""; } tmpStr += ""; if ( event->allDay() ) { if ( event->isMultiDay() ) { tmpStr += ""; tmpStr += ""; } else { tmpStr += ""; tmpStr += ""; } } else { if ( event->isMultiDay() ) { tmpStr += ""; tmpStr += ""; } else { tmpStr += ""; if ( event->hasEndDate() && event->dtStart() != event->dtEnd() ) { tmpStr += ""; } else { tmpStr += ""; } tmpStr += ""; tmpStr += ""; tmpStr += ""; } } tmpStr += ""; if ( event->customProperty( "KABC", "BIRTHDAY" ) == "YES" ) { tmpStr += ""; tmpStr += ""; tmpStr += ""; tmpStr += ""; tmpStr += "
    " + i18n( "Location" ) + "" + event->richLocation() + "
    " + i18n( "Time" ) + "" + i18nc( " - ","%1 - %2", IncidenceFormatter::dateToString( event->dtStart(), true, spec ), IncidenceFormatter::dateToString( event->dtEnd(), true, spec ) ) + "" + i18n( "Date" ) + "" + i18nc( "date as string","%1", IncidenceFormatter::dateToString( event->dtStart(), true, spec ) ) + "" + i18n( "Time" ) + "" + i18nc( " - ","%1 - %2", IncidenceFormatter::dateToString( event->dtStart(), true, spec ), IncidenceFormatter::dateToString( event->dtEnd(), true, spec ) ) + "" + i18n( "Time" ) + "" + i18nc( " - ","%1 - %2", IncidenceFormatter::timeToString( event->dtStart(), true, spec ), IncidenceFormatter::timeToString( event->dtEnd(), true, spec ) ) + "" + IncidenceFormatter::timeToString( event->dtStart(), true, spec ) + "
    " + i18n( "Date" ) + "" + i18nc( "date as string","%1", IncidenceFormatter::dateToString( event->dtStart(), true, spec ) ) + "
    " + i18n( "Birthday" ) + "" + eventViewerFormatBirthday( event ) + "
    "; return tmpStr; } if ( !event->description().isEmpty() ) { tmpStr += ""; tmpStr += ""; tmpStr += "" + eventViewerAddTag( "p", event->richDescription() ) + ""; tmpStr += ""; } if ( event->categories().count() > 0 ) { tmpStr += ""; tmpStr += ""; tmpStr += i18np( "1 category", "%1 categories", event->categories().count() ) + ""; tmpStr += "" + event->categoriesStr() + ""; tmpStr += ""; } if ( event->recurs() ) { KDateTime dt = event->recurrence()->getNextDateTime( KDateTime::currentUtcDateTime() ); tmpStr += ""; tmpStr += "" + i18n( "Next Occurrence" )+ ""; tmpStr += "" + ( dt.isValid() ? KGlobal::locale()->formatDateTime( dt.dateTime(), KLocale::ShortDate ) : i18nc( "no date", "none" ) ) + ""; tmpStr += ""; } tmpStr += ""; tmpStr += eventViewerFormatAttendees( event ); tmpStr += ""; int attachmentCount = event->attachments().count(); if ( attachmentCount > 0 ) { tmpStr += ""; tmpStr += ""; tmpStr += i18np( "1 attachment", "%1 attachments", attachmentCount )+ ""; tmpStr += "" + eventViewerFormatAttachments( event ) + ""; tmpStr += ""; } KDateTime kdt = event->created().toTimeSpec( spec ); tmpStr += ""; tmpStr += "

    " + i18n( "Creation date: %1", KGlobal::locale()->formatDateTime( kdt.dateTime(), KLocale::ShortDate ) ) + ""; return tmpStr; } static QString eventViewerFormatTodo( Todo *todo, KDateTime::Spec spec ) { if ( !todo ) { return QString(); } QString tmpStr = eventViewerFormatHeader( todo ); if ( !todo->location().isEmpty() ) { tmpStr += eventViewerAddTag( "b", i18n(" Location: %1", todo->richLocation() ) ); tmpStr += "
    "; } if ( todo->hasDueDate() && todo->dtDue().isValid() ) { tmpStr += i18n( "Due on: %1", IncidenceFormatter::dateTimeToString( todo->dtDue(), todo->allDay(), true, spec ) ); } if ( !todo->description().isEmpty() ) { tmpStr += eventViewerAddTag( "p", todo->richDescription() ); } tmpStr += eventViewerFormatCategories( todo ); if ( todo->priority() > 0 ) { tmpStr += i18n( "

    Priority: %1

    ", todo->priority() ); } else { tmpStr += i18n( "

    Priority: %1

    ", i18n( "Unspecified" ) ); } tmpStr += i18n( "

    %1 % completed

    ", todo->percentComplete() ); if ( todo->recurs() ) { KDateTime dt = todo->recurrence()->getNextDateTime( KDateTime::currentUtcDateTime() ); tmpStr += eventViewerAddTag( "p", "" + i18n( "This is a recurring to-do. The next occurrence will be on %1.", KGlobal::locale()->formatDateTime( dt.dateTime(), KLocale::ShortDate ) ) + "" ); } tmpStr += eventViewerFormatAttendees( todo ); tmpStr += eventViewerFormatAttachments( todo ); KDateTime kdt = todo->created().toTimeSpec( spec ); - tmpStr += "

    " + i18n( "Creation date: %1", - KGlobal::locale()->formatDateTime( kdt.dateTime(), KLocale::ShortDate ) ) + ""; + tmpStr += "

    " + + i18n( "Creation date: %1", + KGlobal::locale()->formatDateTime( kdt.dateTime(), KLocale::ShortDate ) ) + + ""; return tmpStr; } static QString eventViewerFormatJournal( Journal *journal, KDateTime::Spec spec ) { if ( !journal ) { return QString(); } - QString tmpStr = eventViewerFormatHeader( journal ); - + QString tmpStr; + if ( !journal->summary().isEmpty() ) { + tmpStr+= eventViewerAddTag( "h2", journal->richSummary() ); + } tmpStr += eventViewerAddTag( "h3", i18n( "Journal for %1", IncidenceFormatter::dateToString( journal->dtStart(), false, spec ) ) ); if ( !journal->description().isEmpty() ) { tmpStr += eventViewerAddTag( "p", journal->richDescription() ); } return tmpStr; } static QString eventViewerFormatFreeBusy( FreeBusy *fb, KDateTime::Spec spec ) { Q_UNUSED( spec ); if ( !fb ) { return QString(); } QString tmpStr( eventViewerAddTag( "h2", i18n( "Free/Busy information for %1", fb->organizer().fullName() ) ) ); tmpStr += eventViewerAddTag( "h4", i18n( "Busy times in date range %1 - %2:", KGlobal::locale()->formatDate( fb->dtStart().date(), KLocale::ShortDate ), KGlobal::locale()->formatDate( fb->dtEnd().date(), KLocale::ShortDate ) ) ); QList periods = fb->busyPeriods(); QString text = eventViewerAddTag( "em", eventViewerAddTag( "b", i18nc( "tag for busy periods list", "Busy:" ) ) ); QList::iterator it; for ( it = periods.begin(); it != periods.end(); ++it ) { Period per = *it; if ( per.hasDuration() ) { int dur = per.duration().asSeconds(); QString cont; if ( dur >= 3600 ) { cont += i18ncp( "hours part of duration", "1 hour ", "%1 hours ", dur / 3600 ); dur %= 3600; } if ( dur >= 60 ) { cont += i18ncp( "minutes part duration", "1 minute ", "%1 minutes ", dur / 60 ); dur %= 60; } if ( dur > 0 ) { cont += i18ncp( "seconds part of duration", "1 second", "%1 seconds", dur ); } text += i18nc( "startDate for duration", "%1 for %2", KGlobal::locale()->formatDateTime( per.start().dateTime(), KLocale::LongDate ), cont ); text += "
    "; } else { if ( per.start().date() == per.end().date() ) { text += i18nc( "date, fromTime - toTime ", "%1, %2 - %3", KGlobal::locale()->formatDate( per.start().date() ), KGlobal::locale()->formatTime( per.start().time() ), KGlobal::locale()->formatTime( per.end().time() ) ); } else { text += i18nc( "fromDateTime - toDateTime", "%1 - %2", KGlobal::locale()->formatDateTime( per.start().dateTime(), KLocale::LongDate ), KGlobal::locale()->formatDateTime( per.end().dateTime(), KLocale::LongDate ) ); } text += "
    "; } } tmpStr += eventViewerAddTag( "p", text ); return tmpStr; } //@endcond //@cond PRIVATE class KCal::IncidenceFormatter::EventViewerVisitor : public IncidenceBase::Visitor { public: EventViewerVisitor() : mSpec( KDateTime::Spec() ), mResult( "" ) {} bool act( IncidenceBase *incidence, KDateTime::Spec spec=KDateTime::Spec() ) { mSpec = spec; mResult = ""; return incidence->accept( *this ); } QString result() const { return mResult; } protected: bool visit( Event *event ) { mResult = eventViewerFormatEvent( event, mSpec ); return !mResult.isEmpty(); } bool visit( Todo *todo ) { mResult = eventViewerFormatTodo( todo, mSpec ); return !mResult.isEmpty(); } bool visit( Journal *journal ) { mResult = eventViewerFormatJournal( journal, mSpec ); return !mResult.isEmpty(); } bool visit( FreeBusy *fb ) { mResult = eventViewerFormatFreeBusy( fb, mSpec ); return !mResult.isEmpty(); } protected: KDateTime::Spec mSpec; QString mResult; }; //@endcond QString IncidenceFormatter::extensiveDisplayString( IncidenceBase *incidence ) { return extensiveDisplayStr( incidence, KDateTime::Spec() ); } QString IncidenceFormatter::extensiveDisplayStr( IncidenceBase *incidence, KDateTime::Spec spec ) { if ( !incidence ) { return QString(); } EventViewerVisitor v; if ( v.act( incidence, spec ) ) { return v.result(); } else { return QString(); } } /******************************************************************* * Helper functions for the body part formatter of kmail *******************************************************************/ -//TODO: 4.4: remove "meeting" from the invitation strings - //@cond PRIVATE static QString string2HTML( const QString &str ) { return Qt::escape( str ); } static QString cleanHtml( const QString &html ) { QRegExp rx( "]*>(.*)", Qt::CaseInsensitive ); rx.indexIn( html ); QString body = rx.cap( 1 ); return Qt::escape( body.remove( QRegExp( "<[^>]*>" ) ).trimmed() ); } static QString eventStartTimeStr( Event *event ) { QString tmp; if ( !event->allDay() ) { tmp = i18nc( "%1: Start Date, %2: Start Time", "%1 %2", IncidenceFormatter::dateToString( event->dtStart(), true, KSystemTimeZones::local() ), IncidenceFormatter::timeToString( event->dtStart(), true, KSystemTimeZones::local() ) ); } else { tmp = i18nc( "%1: Start Date", "%1 (all day)", IncidenceFormatter::dateToString( event->dtStart(), true, KSystemTimeZones::local() ) ); } return tmp; } static QString eventEndTimeStr( Event *event ) { QString tmp; if ( event->hasEndDate() && event->dtEnd().isValid() ) { if ( !event->allDay() ) { tmp = i18nc( "%1: End Date, %2: End Time", "%1 %2", IncidenceFormatter::dateToString( event->dtEnd(), true, KSystemTimeZones::local() ), IncidenceFormatter::timeToString( event->dtEnd(), true, KSystemTimeZones::local() ) ); } else { tmp = i18nc( "%1: End Date", "%1 (all day)", IncidenceFormatter::dateToString( event->dtEnd(), true, KSystemTimeZones::local() ) ); } - } else { - tmp = i18n( "Unspecified" ); } return tmp; } static QString invitationRow( const QString &cell1, const QString &cell2 ) { return "" + cell1 + "" + cell2 + "\n"; } +static bool iamOrganizer( Incidence *incidence ) +{ + // Check if I'm the organizer for this incidence + + if ( !incidence ) { + return false; + } + + bool iam = false; + KEMailSettings settings; + QStringList profiles = settings.profiles(); + for ( QStringList::Iterator it=profiles.begin(); it != profiles.end(); ++it ) { + settings.setProfile( *it ); + if ( settings.getSetting( KEMailSettings::EmailAddress ) == incidence->organizer().email() ) { + iam = true; + break; + } + } + return iam; +} + +static bool iamAttendee( Attendee *attendee ) +{ + // Check if I'm this attendee + + bool iam = false; + KEMailSettings settings; + QStringList profiles = settings.profiles(); + for ( QStringList::Iterator it=profiles.begin(); it != profiles.end(); ++it ) { + settings.setProfile( *it ); + if ( settings.getSetting( KEMailSettings::EmailAddress ) == attendee->email() ) { + iam = true; + break; + } + } + return iam; +} + +static Attendee *findMyAttendee( Incidence *incidence ) +{ + // Return the attendee for the incidence that is probably me + + Attendee *attendee = 0; + if ( !incidence ) { + return attendee; + } + + KEMailSettings settings; + QStringList profiles = settings.profiles(); + for ( QStringList::Iterator it=profiles.begin(); it != profiles.end(); ++it ) { + settings.setProfile( *it ); + + Attendee::List attendees = incidence->attendees(); + Attendee::List::ConstIterator it2; + for ( it2 = attendees.constBegin(); it2 != attendees.constEnd(); ++it2 ) { + Attendee *a = *it2; + if ( settings.getSetting( KEMailSettings::EmailAddress ) == a->email() ) { + attendee = a; + break; + } + } + } + return attendee; +} + +static Attendee *findAttendee( Incidence *incidence, const QString &email ) +{ + // Search for an attendee by email address + + Attendee *attendee = 0; + if ( !incidence ) { + return attendee; + } + + Attendee::List attendees = incidence->attendees(); + Attendee::List::ConstIterator it; + for ( it = attendees.constBegin(); it != attendees.constEnd(); ++it ) { + Attendee *a = *it; + if ( email == a->email() ) { + attendee = a; + break; + } + } + return attendee; +} + +static bool rsvpRequested( Incidence *incidence ) +{ + //use a heuristic to determine if a response is requested. + + bool rsvp = true; // better send superfluously than not at all + Attendee::List attendees = incidence->attendees(); + Attendee::List::ConstIterator it; + for ( it = attendees.constBegin(); it != attendees.constEnd(); ++it ) { + if ( it == attendees.constBegin() ) { + rsvp = (*it)->RSVP(); // use what the first one has + } else { + if ( (*it)->RSVP() != rsvp ) { + rsvp = true; // they differ, default + break; + } + } + } + return rsvp; +} + +static QString rsvpRequestedStr( bool rsvpRequested ) +{ + if ( rsvpRequested ) { + return i18n( "Your response is requested" ); + } else { + return i18n( "A response is not necessary" ); + } +} + +static QString invitationPerson( const QString &email, QString name, QString uid ) +{ + // Make the search, if there is an email address to search on, + // and either name or uid is missing + if ( !email.isEmpty() && ( name.isEmpty() || uid.isEmpty() ) ) { + KABC::AddressBook *add_book = KABC::StdAddressBook::self( true ); + KABC::Addressee::List addressList = add_book->findByEmail( email ); + if ( !addressList.isEmpty() ) { + KABC::Addressee o = addressList.first(); + if ( !o.isEmpty() && addressList.size() < 2 ) { + if ( name.isEmpty() ) { + // No name set, so use the one from the addressbook + name = o.formattedName(); + } + uid = o.uid(); + } else { + // Email not found in the addressbook. Don't make a link + uid.clear(); + } + } + } + + // Show the attendee + QString tmpString; + if ( !uid.isEmpty() ) { + // There is a UID, so make a link to the addressbook + if ( name.isEmpty() ) { + // Use the email address for text + tmpString += eventViewerAddLink( "uid:" + uid, email ); + } else { + tmpString += eventViewerAddLink( "uid:" + uid, name ); + } + } else { + // No UID, just show some text + tmpString += ( name.isEmpty() ? email : name ); + } + tmpString += '\n'; + + // Make the mailto link + if ( !email.isEmpty() ) { + KCal::Person person( name, email ); + KUrl mailto; + mailto.setProtocol( "mailto" ); + mailto.setPath( person.fullName() ); + const QString iconPath = + KIconLoader::global()->iconPath( "mail-message-new", KIconLoader::Small ); + tmpString += eventViewerAddLink( mailto.url(), "" ); + } + tmpString += '\n'; + + return tmpString; +} + static QString invitationsDetailsIncidence( Incidence *incidence, bool noHtmlMode ) { // if description and comment -> use both // if description, but no comment -> use the desc as the comment (and no desc) // if comment, but no description -> use the comment and no description QString html; QString descr; QStringList comments; if ( incidence->comments().isEmpty() ) { if ( !incidence->description().isEmpty() ) { // use description as comments if ( !incidence->descriptionIsRich() ) { comments << string2HTML( incidence->description() ); } else { comments << incidence->richDescription(); if ( noHtmlMode ) { comments[0] = cleanHtml( comments[0] ); } + comments[0] = eventViewerAddTag( "p", comments[0] ); } } //else desc and comments are empty } else { // non-empty comments for ( int i=0; i < incidence->comments().count(); ++i ) { comments[i] = string2HTML( incidence->comments()[i] ); } if ( !incidence->description().isEmpty() ) { // use description too if ( !incidence->descriptionIsRich() ) { descr = string2HTML( incidence->description() ); } else { descr = incidence->richDescription(); if ( noHtmlMode ) { descr = cleanHtml( descr ); } + descr = eventViewerAddTag( "p", descr ); } } } if( !descr.isEmpty() ) { html += "

    "; html += ""; html += ""; html += ""; html += "
    " + eventViewerAddTag( "u", i18n( "Description:" ) ) + "
    " + descr + "
    "; } if ( !comments.isEmpty() ) { html += "

    "; html += ""; html += ""; - html += ""; + html += ""; + html += ""; } else { - html += ""; + html += comments[0]; } - html += ""; + html += ""; html += "
    " + eventViewerAddTag( "u", i18n( "Comments:" ) ) + "
    "; if ( comments.count() > 1 ) { - html += "
      "; + html += "
        "; for ( int i=0; i < comments.count(); ++i ) { html += "
      • " + comments[i] + "
      • "; } - html += "
    " + comments[0] + "
    "; } return html; } -static QString invitationDetailsEvent( Event *event, bool noHtmlMode ) +static QString invitationDetailsEvent( Event *event, bool noHtmlMode, KDateTime::Spec spec ) { - // Meeting details are formatted into an HTML table + // Invitation details are formatted into an HTML table if ( !event ) { return QString(); } QString sSummary = i18n( "Summary unspecified" ); if ( !event->summary().isEmpty() ) { if ( !event->summaryIsRich() ) { sSummary = string2HTML( event->summary() ); } else { sSummary = event->richSummary(); if ( noHtmlMode ) { sSummary = cleanHtml( sSummary ); } } } QString sLocation = i18n( "Location unspecified" ); if ( !event->location().isEmpty() ) { if ( !event->locationIsRich() ) { sLocation = string2HTML( event->location() ); } else { sLocation = event->richLocation(); if ( noHtmlMode ) { sLocation = cleanHtml( sLocation ); } } } QString dir = ( QApplication::isRightToLeft() ? "rtl" : "ltr" ); QString html = QString( "

    \n" ).arg( dir ); + html += ""; - // Meeting summary & location rows + // Invitation summary & location rows html += invitationRow( i18n( "What:" ), sSummary ); html += invitationRow( i18n( "Where:" ), sLocation ); - // Meeting Start Time Row - html += invitationRow( i18n( "Start Time:" ), eventStartTimeStr( event ) ); - - // Meeting End Time Row - html += invitationRow( i18n( "End Time:" ), eventEndTimeStr( event ) ); + // If a 1 day event + if ( event->dtStart().date() == event->dtEnd().date() ) { + html += invitationRow( i18n( "Date:" ), + IncidenceFormatter::dateToString( event->dtStart(), false, spec ) ); + if ( !event->allDay() ) { + html += invitationRow( i18n( "Time:" ), + IncidenceFormatter::timeToString( event->dtStart(), false, spec ) + + " - " + + IncidenceFormatter::timeToString( event->dtEnd(), false, spec ) ); + } + } else { + html += invitationRow( i18nc( "starting date", "From:" ), + IncidenceFormatter::dateToString( event->dtStart(), false, spec ) ); + if ( !event->allDay() ) { + html += invitationRow( i18nc( "starting time", "At:" ), + IncidenceFormatter::timeToString( event->dtStart(), false, spec ) ); + } + if ( event->hasEndDate() ) { + html += invitationRow( i18nc( "ending date", "To:" ), + IncidenceFormatter::dateToString( event->dtEnd(), false, spec ) ); + if ( !event->allDay() ) { + html += invitationRow( i18nc( "ending time", "At:" ), + IncidenceFormatter::timeToString( event->dtEnd(), false, spec ) ); + } + } else { + html += invitationRow( i18nc( "ending date", "To:" ), + i18n( "no end date specified" ) ); + } + } - // Meeting Duration Row + // Invitation Duration Row if ( !event->allDay() && event->hasEndDate() && event->dtEnd().isValid() ) { QString tmp; QTime sDuration( 0, 0, 0 ), t; int secs = event->dtStart().secsTo( event->dtEnd() ); t = sDuration.addSecs( secs ); if ( t.hour() > 0 ) { tmp += i18np( "1 hour ", "%1 hours ", t.hour() ); } if ( t.minute() > 0 ) { tmp += i18np( "1 minute ", "%1 minutes ", t.minute() ); } html += invitationRow( i18n( "Duration:" ), tmp ); } if ( event->recurs() ) { html += invitationRow( i18n( "Recurrence:" ), IncidenceFormatter::recurrenceString( event ) ); } - html += ""; + html += "
    \n"; html += invitationsDetailsIncidence( event, noHtmlMode ); return html; } -static QString invitationDetailsTodo( Todo *todo, bool noHtmlMode ) +static QString invitationDetailsTodo( Todo *todo, bool noHtmlMode, KDateTime::Spec spec ) { // To-do details are formatted into an HTML table if ( !todo ) { return QString(); } QString sSummary = i18n( "Summary unspecified" ); QString sDescr = i18n( "Description unspecified" ); if ( ! todo->summary().isEmpty() ) { sSummary = todo->richSummary(); if ( noHtmlMode ) { sSummary = cleanHtml( sSummary ); } } if ( ! todo->description().isEmpty() ) { sDescr = todo->description(); if ( noHtmlMode ) { sDescr = cleanHtml( sDescr ); } } - QString html = ""; + QString html( "
    \n" ); html += invitationRow( i18n( "Summary:" ), sSummary ); + if ( todo->hasStartDate() && todo->dtStart().isValid() ) { + html += invitationRow( i18n( "Start Date:" ), + IncidenceFormatter::dateToString( todo->dtStart(), + false, + spec ) ); + } else { + html += invitationRow( i18n( "Start Date:" ), + i18nc( "no to-do start date", "None" ) ); + } + if ( todo->hasDueDate() && todo->dtDue().isValid() ) { + html += invitationRow( i18n( "Due Date:" ), + IncidenceFormatter::dateToString( todo->dtDue(), + false, + spec ) ); + } else { + html += invitationRow( i18n( "Due Date:" ), + i18nc( "no to-do due date", "None" ) ); + } html += invitationRow( i18n( "Description:" ), sDescr ); html += "
    \n"; html += invitationsDetailsIncidence( todo, noHtmlMode ); return html; } -static QString invitationDetailsJournal( Journal *journal, bool noHtmlMode ) +static QString invitationDetailsJournal( Journal *journal, bool noHtmlMode, KDateTime::Spec spec ) { if ( !journal ) { return QString(); } QString sSummary = i18n( "Summary unspecified" ); QString sDescr = i18n( "Description unspecified" ); if ( ! journal->summary().isEmpty() ) { sSummary = journal->richSummary(); if ( noHtmlMode ) { sSummary = cleanHtml( sSummary ); } } if ( ! journal->description().isEmpty() ) { sDescr = journal->richDescription(); if ( noHtmlMode ) { sDescr = cleanHtml( sDescr ); } } - QString html = ""; + QString html( "
    \n" ); html += invitationRow( i18n( "Summary:" ), sSummary ); html += invitationRow( i18n( "Date:" ), - IncidenceFormatter::dateToString( journal->dtStart(), false, - journal->dtStart().timeSpec() ) ); + IncidenceFormatter::dateToString( journal->dtStart(), + false, + spec ) ); html += invitationRow( i18n( "Description:" ), sDescr ); html += "
    \n"; html += invitationsDetailsIncidence( journal, noHtmlMode ); return html; } -static QString invitationDetailsFreeBusy( FreeBusy *fb ) +static QString invitationDetailsFreeBusy( FreeBusy *fb, bool noHtmlMode, KDateTime::Spec spec ) { + Q_UNUSED( noHtmlMode ); + if ( !fb ) { return QString(); } - QString html = ""; + QString html( "
    \n" ); html += invitationRow( i18n( "Person:" ), fb->organizer().fullName() ); html += invitationRow( i18n( "Start date:" ), IncidenceFormatter::dateToString( fb->dtStart(), - true, - fb->dtStart().timeSpec() ) ); + true, spec ) ); html += invitationRow( i18n( "End date:" ), - KGlobal::locale()->formatDate( fb->dtEnd().date(), KLocale::ShortDate ) ); + IncidenceFormatter::dateToString( fb->dtEnd(), + true, spec ) ); + html += "\n"; html += "\n"; QList periods = fb->busyPeriods(); QList::iterator it; for ( it = periods.begin(); it != periods.end(); ++it ) { Period per = *it; if ( per.hasDuration() ) { int dur = per.duration().asSeconds(); QString cont; if ( dur >= 3600 ) { cont += i18ncp( "hours part of duration", "1 hour ", "%1 hours ", dur / 3600 ); dur %= 3600; } if ( dur >= 60 ) { cont += i18ncp( "minutes part of duration", "1 minute", "%1 minutes ", dur / 60 ); dur %= 60; } if ( dur > 0 ) { cont += i18ncp( "seconds part of duration", "1 second", "%1 seconds", dur ); } html += invitationRow( QString(), i18nc( "startDate for duration", "%1 for %2", KGlobal::locale()->formatDateTime( per.start().dateTime(), KLocale::LongDate ), cont ) ); } else { QString cont; if ( per.start().date() == per.end().date() ) { cont = i18nc( "date, fromTime - toTime ", "%1, %2 - %3", KGlobal::locale()->formatDate( per.start().date() ), KGlobal::locale()->formatTime( per.start().time() ), KGlobal::locale()->formatTime( per.end().time() ) ); } else { cont = i18nc( "fromDateTime - toDateTime", "%1 - %2", KGlobal::locale()->formatDateTime( per.start().dateTime(), KLocale::LongDate ), KGlobal::locale()->formatDateTime( per.end().dateTime(), KLocale::LongDate ) ); } html += invitationRow( QString(), cont ); } } html += "

    Busy periods given in this free/busy object:
    \n"; return html; } static QString invitationHeaderEvent( Event *event, ScheduleMessage *msg ) { if ( !msg || !event ) { return QString(); } switch ( msg->method() ) { case iTIPPublish: return i18n( "This event has been published" ); case iTIPRequest: if ( event->revision() > 0 ) { - //TODO: 4.4, remove the h3 tag - return i18n( "

    This meeting has been updated

    " ); + return i18n( "This invitation has been updated" ); + } + if ( iamOrganizer( event ) ) { + return i18n( "I sent this invitation" ); } else { - return i18n( "You have been invited to this meeting" ); + if ( !event->organizer().fullName().isEmpty() ) { + return i18n( "You received an invitation from %1", + event->organizer().fullName() ); + } else { + return i18n( "You received an invitation" ); + } } case iTIPRefresh: return i18n( "This invitation was refreshed" ); case iTIPCancel: - return i18n( "This meeting has been canceled" ); + return i18n( "This invitation has been canceled" ); case iTIPAdd: - return i18n( "Addition to the meeting invitation" ); + return i18n( "Addition to the invitation" ); case iTIPReply: { Attendee::List attendees = event->attendees(); if( attendees.count() == 0 ) { kDebug() << "No attendees in the iCal reply!"; return QString(); } if ( attendees.count() != 1 ) { kDebug() << "Warning: attendeecount in the reply should be 1" << "but is" << attendees.count(); } Attendee *attendee = *attendees.begin(); QString attendeeName = attendee->name(); if ( attendeeName.isEmpty() ) { attendeeName = attendee->email(); } if ( attendeeName.isEmpty() ) { attendeeName = i18n( "Sender" ); } QString delegatorName, dummy; KPIMUtils::extractEmailAddressAndName( attendee->delegator(), dummy, delegatorName ); if ( delegatorName.isEmpty() ) { delegatorName = attendee->delegator(); } switch( attendee->status() ) { case Attendee::NeedsAction: return i18n( "%1 indicates this invitation still needs some action", attendeeName ); case Attendee::Accepted: if ( delegatorName.isEmpty() ) { - return i18n( "%1 accepts this meeting invitation", attendeeName ); + return i18n( "%1 accepts this invitation", attendeeName ); + } else { + return i18n( "%1 accepts this invitation on behalf of %2", + attendeeName, delegatorName ); } - return i18n( "%1 accepts this meeting invitation on behalf of %2", - attendeeName, delegatorName ); case Attendee::Tentative: if ( delegatorName.isEmpty() ) { - return i18n( "%1 tentatively accepts this meeting invitation", attendeeName ); + return i18n( "%1 tentatively accepts this invitation", attendeeName ); + } else { + return i18n( "%1 tentatively accepts this invitation on behalf of %2", + attendeeName, delegatorName ); } - return i18n( "%1 tentatively accepts this meeting invitation on behalf of %2", - attendeeName, delegatorName ); case Attendee::Declined: if ( delegatorName.isEmpty() ) { - return i18n( "%1 declines this meeting invitation", attendeeName ); + return i18n( "%1 declines this invitation", attendeeName ); + } else { + return i18n( "%1 declines this invitation on behalf of %2", + attendeeName, delegatorName ); } - return i18n( "%1 declines this meeting invitation on behalf of %2", - attendeeName, delegatorName ); case Attendee::Delegated: { QString delegate, dummy; KPIMUtils::extractEmailAddressAndName( attendee->delegate(), dummy, delegate ); if ( delegate.isEmpty() ) { delegate = attendee->delegate(); } if ( !delegate.isEmpty() ) { - return i18n( "%1 has delegated this meeting invitation to %2", attendeeName, delegate ); + return i18n( "%1 has delegated this invitation to %2", attendeeName, delegate ); + } else { + return i18n( "%1 has delegated this invitation", attendeeName ); } - return i18n( "%1 has delegated this meeting invitation", attendeeName ); } case Attendee::Completed: - return i18n( "This meeting invitation is now completed" ); + return i18n( "This invitation is now completed" ); case Attendee::InProcess: return i18n( "%1 is still processing the invitation", attendeeName ); default: - return i18n( "Unknown response to this meeting invitation" ); + return i18n( "Unknown response to this invitation" ); } break; } case iTIPCounter: return i18n( "Sender makes this counter proposal" ); case iTIPDeclineCounter: return i18n( "Sender declines the counter proposal" ); case iTIPNoMethod: return i18n( "Error: iMIP message with unknown method: '%1'", msg->method() ); } return QString(); } static QString invitationHeaderTodo( Todo *todo, ScheduleMessage *msg ) { if ( !msg || !todo ) { return QString(); } switch ( msg->method() ) { case iTIPPublish: return i18n( "This to-do has been published" ); case iTIPRequest: if ( todo->revision() > 0 ) { return i18n( "This to-do has been updated" ); } else { return i18n( "You have been assigned this to-do" ); } case iTIPRefresh: return i18n( "This to-do was refreshed" ); case iTIPCancel: return i18n( "This to-do was canceled" ); case iTIPAdd: return i18n( "Addition to the to-do" ); case iTIPReply: { Attendee::List attendees = todo->attendees(); if ( attendees.count() == 0 ) { kDebug() << "No attendees in the iCal reply!"; return QString(); } if ( attendees.count() != 1 ) { kDebug() << "Warning: attendeecount in the reply should be 1" << "but is" << attendees.count(); } Attendee *attendee = *attendees.begin(); + switch( attendee->status() ) { case Attendee::NeedsAction: return i18n( "Sender indicates this to-do assignment still needs some action" ); case Attendee::Accepted: return i18n( "Sender accepts this to-do" ); case Attendee::Tentative: return i18n( "Sender tentatively accepts this to-do" ); case Attendee::Declined: return i18n( "Sender declines this to-do" ); case Attendee::Delegated: { QString delegate, dummy; KPIMUtils::extractEmailAddressAndName( attendee->delegate(), dummy, delegate ); if ( delegate.isEmpty() ) { delegate = attendee->delegate(); } if ( !delegate.isEmpty() ) { return i18n( "Sender has delegated this request for the to-do to %1", delegate ); + } else { + return i18n( "Sender has delegated this request for the to-do " ); } - return i18n( "Sender has delegated this request for the to-do " ); } case Attendee::Completed: return i18n( "The request for this to-do is now completed" ); case Attendee::InProcess: return i18n( "Sender is still processing the invitation" ); default: return i18n( "Unknown response to this to-do" ); } break; } case iTIPCounter: return i18n( "Sender makes this counter proposal" ); case iTIPDeclineCounter: return i18n( "Sender declines the counter proposal" ); case iTIPNoMethod: return i18n( "Error: iMIP message with unknown method: '%1'", msg->method() ); } return QString(); } static QString invitationHeaderJournal( Journal *journal, ScheduleMessage *msg ) { // TODO: Several of the methods are not allowed for journals, so remove them. if ( !msg || !journal ) { return QString(); } switch ( msg->method() ) { case iTIPPublish: return i18n( "This journal has been published" ); case iTIPRequest: return i18n( "You have been assigned this journal" ); case iTIPRefresh: return i18n( "This journal was refreshed" ); case iTIPCancel: return i18n( "This journal was canceled" ); case iTIPAdd: return i18n( "Addition to the journal" ); case iTIPReply: { Attendee::List attendees = journal->attendees(); if ( attendees.count() == 0 ) { kDebug() << "No attendees in the iCal reply!"; return QString(); } - if( attendees.count() != 1 ) { - kDebug() << "Warning: attendeecount in the reply should be 1" - << "but is" << attendees.count(); + kDebug() << "Warning: attendeecount in the reply should be 1 " + << "but is " << attendees.count(); } - Attendee *attendee = *attendees.begin(); + switch( attendee->status() ) { case Attendee::NeedsAction: return i18n( "Sender indicates this journal assignment still needs some action" ); case Attendee::Accepted: return i18n( "Sender accepts this journal" ); case Attendee::Tentative: return i18n( "Sender tentatively accepts this journal" ); case Attendee::Declined: return i18n( "Sender declines this journal" ); case Attendee::Delegated: return i18n( "Sender has delegated this request for the journal" ); case Attendee::Completed: return i18n( "The request for this journal is now completed" ); case Attendee::InProcess: return i18n( "Sender is still processing the invitation" ); default: return i18n( "Unknown response to this journal" ); } break; } case iTIPCounter: return i18n( "Sender makes this counter proposal" ); case iTIPDeclineCounter: return i18n( "Sender declines the counter proposal" ); case iTIPNoMethod: return i18n( "Error: iMIP message with unknown method: '%1'", msg->method() ); } return QString(); } static QString invitationHeaderFreeBusy( FreeBusy *fb, ScheduleMessage *msg ) { if ( !msg || !fb ) { return QString(); } switch ( msg->method() ) { case iTIPPublish: return i18n( "This free/busy list has been published" ); case iTIPRequest: return i18n( "The free/busy list has been requested" ); case iTIPRefresh: return i18n( "This free/busy list was refreshed" ); case iTIPCancel: return i18n( "This free/busy list was canceled" ); case iTIPAdd: return i18n( "Addition to the free/busy list" ); case iTIPNoMethod: default: return i18n( "Error: Free/Busy iMIP message with unknown method: '%1'", msg->method() ); } } //@endcond +static QString invitationAttendees( Incidence *incidence ) +{ + QString tmpStr; + if ( !incidence ) { + return tmpStr; + } + + tmpStr += i18n( "Invitation List" ); + + int count=0; + Attendee::List attendees = incidence->attendees(); + if ( !attendees.isEmpty() ) { + + Attendee::List::ConstIterator it; + for ( it = attendees.constBegin(); it != attendees.constEnd(); ++it ) { + Attendee *a = *it; + if ( !iamAttendee( a ) ) { + count++; + if ( count == 1 ) { + tmpStr += ""; + } + tmpStr += ""; + tmpStr += ""; + tmpStr += ""; + tmpStr += ""; + } + } + } + if ( count ) { + tmpStr += "
    "; + tmpStr += invitationPerson( a->email(), a->name(), QString() ); + if ( !a->delegator().isEmpty() ) { + tmpStr += i18n( " (delegated by %1)", a->delegator() ); + } + if ( !a->delegate().isEmpty() ) { + tmpStr += i18n( " (delegated to %1)", a->delegate() ); + } + tmpStr += "" + a->statusStr() + "
    "; + } else { + tmpStr += "" + i18nc( "no attendees", "None" ) + ""; + } + + return tmpStr; +} + //@cond PRIVATE class KCal::IncidenceFormatter::ScheduleMessageVisitor : public IncidenceBase::Visitor { public: ScheduleMessageVisitor() : mMessage(0) { mResult = ""; } bool act( IncidenceBase *incidence, ScheduleMessage *msg ) { mMessage = msg; return incidence->accept( *this ); } QString result() const { return mResult; } protected: QString mResult; ScheduleMessage *mMessage; }; class KCal::IncidenceFormatter::InvitationHeaderVisitor : public IncidenceFormatter::ScheduleMessageVisitor { protected: bool visit( Event *event ) { mResult = invitationHeaderEvent( event, mMessage ); return !mResult.isEmpty(); } bool visit( Todo *todo ) { mResult = invitationHeaderTodo( todo, mMessage ); return !mResult.isEmpty(); } bool visit( Journal *journal ) { mResult = invitationHeaderJournal( journal, mMessage ); return !mResult.isEmpty(); } bool visit( FreeBusy *fb ) { mResult = invitationHeaderFreeBusy( fb, mMessage ); return !mResult.isEmpty(); } }; class KCal::IncidenceFormatter::InvitationBodyVisitor : public IncidenceFormatter::ScheduleMessageVisitor { public: - InvitationBodyVisitor( bool noHtmlMode ) - : ScheduleMessageVisitor(), mNoHtmlMode( noHtmlMode ) { } + InvitationBodyVisitor( bool noHtmlMode, KDateTime::Spec spec ) + : ScheduleMessageVisitor(), mNoHtmlMode( noHtmlMode ), mSpec( spec ) {} protected: bool visit( Event *event ) { - mResult = invitationDetailsEvent( event, mNoHtmlMode ); + mResult = invitationDetailsEvent( event, mNoHtmlMode, mSpec ); return !mResult.isEmpty(); } bool visit( Todo *todo ) { - mResult = invitationDetailsTodo( todo, mNoHtmlMode ); + mResult = invitationDetailsTodo( todo, mNoHtmlMode, mSpec ); return !mResult.isEmpty(); } bool visit( Journal *journal ) { - mResult = invitationDetailsJournal( journal, mNoHtmlMode ); + mResult = invitationDetailsJournal( journal, mNoHtmlMode, mSpec ); return !mResult.isEmpty(); } bool visit( FreeBusy *fb ) { - mResult = invitationDetailsFreeBusy( fb ); + mResult = invitationDetailsFreeBusy( fb, mNoHtmlMode, mSpec ); return !mResult.isEmpty(); } private: bool mNoHtmlMode; + KDateTime::Spec mSpec; }; //@endcond QString InvitationFormatterHelper::generateLinkURL( const QString &id ) { return id; } //@cond PRIVATE class IncidenceFormatter::IncidenceCompareVisitor : public IncidenceBase::Visitor { public: IncidenceCompareVisitor() : mExistingIncidence( 0 ) {} bool act( IncidenceBase *incidence, Incidence *existingIncidence ) { if ( !existingIncidence ) { return false; } Incidence *inc = dynamic_cast( incidence ); - if ( inc && inc->revision() <= existingIncidence->revision() ) { + if ( !inc || !existingIncidence || inc->revision() <= existingIncidence->revision() ) { return false; } mExistingIncidence = existingIncidence; return incidence->accept( *this ); } QString result() const { if ( mChanges.isEmpty() ) { return QString(); } QString html = "
    • "; html += mChanges.join( "
    • " ); html += "
      "; return html; } protected: bool visit( Event *event ) { compareEvents( event, dynamic_cast( mExistingIncidence ) ); compareIncidences( event, mExistingIncidence ); return !mChanges.isEmpty(); } bool visit( Todo *todo ) { compareIncidences( todo, mExistingIncidence ); return !mChanges.isEmpty(); } bool visit( Journal *journal ) { compareIncidences( journal, mExistingIncidence ); return !mChanges.isEmpty(); } bool visit( FreeBusy *fb ) { Q_UNUSED( fb ); return !mChanges.isEmpty(); } private: void compareEvents( Event *newEvent, Event *oldEvent ) { if ( !oldEvent || !newEvent ) { return; } if ( oldEvent->dtStart() != newEvent->dtStart() || oldEvent->allDay() != newEvent->allDay() ) { - mChanges += i18n( "The begin of the meeting has been changed from %1 to %2", + mChanges += i18n( "The invitation starting time has been changed from %1 to %2", eventStartTimeStr( oldEvent ), eventStartTimeStr( newEvent ) ); } if ( oldEvent->dtEnd() != newEvent->dtEnd() || oldEvent->allDay() != newEvent->allDay() ) { - mChanges += i18n( "The end of the meeting has been changed from %1 to %2", + mChanges += i18n( "The invitation ending time has been changed from %1 to %2", eventEndTimeStr( oldEvent ), eventEndTimeStr( newEvent ) ); } } void compareIncidences( Incidence *newInc, Incidence *oldInc ) { if ( !oldInc || !newInc ) { return; } if ( oldInc->summary() != newInc->summary() ) { mChanges += i18n( "The summary has been changed to: \"%1\"", newInc->richSummary() ); } if ( oldInc->location() != newInc->location() ) { mChanges += i18n( "The location has been changed to: \"%1\"", newInc->richLocation() ); } if ( oldInc->description() != newInc->description() ) { mChanges += i18n( "The description has been changed to: \"%1\"", newInc->richDescription() ); } Attendee::List oldAttendees = oldInc->attendees(); Attendee::List newAttendees = newInc->attendees(); for ( Attendee::List::ConstIterator it = newAttendees.constBegin(); it != newAttendees.constEnd(); ++it ) { Attendee *oldAtt = oldInc->attendeeByMail( (*it)->email() ); if ( !oldAtt ) { mChanges += i18n( "Attendee %1 has been added", (*it)->fullName() ); } else { if ( oldAtt->status() != (*it)->status() ) { mChanges += i18n( "The status of attendee %1 has been changed to: %2", (*it)->fullName(), (*it)->statusStr() ); } } } for ( Attendee::List::ConstIterator it = oldAttendees.constBegin(); it != oldAttendees.constEnd(); ++it ) { Attendee *newAtt = newInc->attendeeByMail( (*it)->email() ); if ( !newAtt ) { mChanges += i18n( "Attendee %1 has been removed", (*it)->fullName() ); } } } private: Incidence *mExistingIncidence; QStringList mChanges; }; //@endcond QString InvitationFormatterHelper::makeLink( const QString &id, const QString &text ) { QString res( "%2" ); return res.arg( generateLinkURL( id ) ).arg( text ); return res; } Calendar *InvitationFormatterHelper::calendar() const { return 0; } -//@cond PRIVATE -// Check if the given incidence is likely one that we own instead one from -// a shared calendar (Kolab-specific) -static bool incidenceOwnedByMe( Calendar *calendar, Incidence *incidence ) -{ - CalendarResources* cal = dynamic_cast( calendar ); - if ( !cal || !incidence ) { - return true; - } - - ResourceCalendar *res = cal->resource( incidence ); - if ( !res ) { - return true; - } - - const QString subRes = res->subresourceIdentifier( incidence ); - if ( !subRes.contains( "/.INBOX.directory/" ) ) { - return false; - } - return true; -} - -static QString formatICalInvitationHelper( QString invitation, Calendar *mCalendar, - InvitationFormatterHelper *helper, bool noHtmlMode ) +static QString formatICalInvitationHelper( QString invitation, + Calendar *mCalendar, + InvitationFormatterHelper *helper, + bool noHtmlMode, + KDateTime::Spec spec ) { if ( invitation.isEmpty() ) { return QString(); } ICalFormat format; // parseScheduleMessage takes the tz from the calendar, // no need to set it manually here for the format! ScheduleMessage *msg = format.parseScheduleMessage( mCalendar, invitation ); if( !msg ) { kDebug() << "Failed to parse the scheduling message"; Q_ASSERT( format.exception() ); kDebug() << format.exception()->message(); return QString(); } IncidenceBase *incBase = msg->event(); incBase->shiftTimes( mCalendar->timeSpec(), KDateTime::Spec::LocalZone() ); + // Determine if this incidence is in my calendar Incidence *existingIncidence = 0; - if ( helper->calendar() ) { + if ( incBase && helper->calendar() ) { existingIncidence = helper->calendar()->incidence( incBase->uid() ); - if ( !incidenceOwnedByMe( helper->calendar(), existingIncidence ) ) { - existingIncidence = 0; - } if ( !existingIncidence ) { const Incidence::List list = helper->calendar()->incidences(); for ( Incidence::List::ConstIterator it = list.begin(), end = list.end(); it != end; ++it ) { - if ( (*it)->schedulingID() == incBase->uid() && - incidenceOwnedByMe( helper->calendar(), *it ) ) { + if ( (*it)->schedulingID() == incBase->uid() ) { existingIncidence = *it; break; } } } } // First make the text of the message QString html; - html += "
      "; - html += ""; + html += "
      "; IncidenceFormatter::InvitationHeaderVisitor headerVisitor; // The InvitationHeaderVisitor returns false if the incidence is somehow invalid, or not handled if ( !headerVisitor.act( incBase, msg ) ) { return QString(); } - html += "
      " + eventViewerAddTag( "h3", headerVisitor.result() ) + ""; + html += eventViewerAddTag( "h3", headerVisitor.result() ); - IncidenceFormatter::InvitationBodyVisitor bodyVisitor( noHtmlMode ); + IncidenceFormatter::InvitationBodyVisitor bodyVisitor( noHtmlMode, spec ); if ( !bodyVisitor.act( incBase, msg ) ) { return QString(); } html += bodyVisitor.result(); if ( msg->method() == iTIPRequest ) { // ### Scheduler::Publish/Refresh/Add as well? IncidenceFormatter::IncidenceCompareVisitor compareVisitor; if ( compareVisitor.act( incBase, existingIncidence ) ) { html += i18n( "

      The following changes have been made by the organizer:

      " ); html += compareVisitor.result(); } } + Incidence *inc = dynamic_cast( incBase ); + + // determine if I am the organizer for this invitation + bool myInc = iamOrganizer( inc ); + + // determine if the invitation response has already been recorded + bool rsvpRec = false; + Attendee *ea = 0; + if ( !myInc ) { + if ( existingIncidence ) { + ea = findMyAttendee( existingIncidence ); + } + if ( ea && ( ea->status() == Attendee::Accepted || ea->status() == Attendee::Declined ) ) { + rsvpRec = true; + } + } + + // Print if RSVP needed, not-needed, or response already recorded + bool rsvpReq = rsvpRequested( inc ); + if ( !myInc ) { + html += "
      "; + html += ""; + if ( rsvpRec && ( inc && inc->revision() == 0 ) ) { + html += i18n( "Your response has already been recorded [%1]", ea->statusStr() ); + rsvpReq = false; + } else { + html += rsvpRequestedStr( rsvpReq ); + } + html += "
      "; + } + // Add groupware links html += "

      "; html += "

      "; - //TODO: 4.4, in tdOpen, change border-width:0px to border-width:2px and also - //remove the [] on the button strings: "Accept", "Decline", "Counter", etc. - const QString tdOpen = ""; - Incidence *incidence = dynamic_cast( incBase ); switch ( msg->method() ) { - case iTIPPublish: - case iTIPRequest: - case iTIPRefresh: - case iTIPAdd: - { - if ( incidence && incidence->revision() > 0 && ( existingIncidence || !helper->calendar() ) ) { - html += tdOpen; - if ( incBase->type() == "Todo" ) { - //TODO: 4.4, remove the [] - html += helper->makeLink( "reply", i18n( "[Enter this into my to-do list]" ) ); - } else { - html += helper->makeLink( "reply", i18n( "[Enter this into my calendar]" ) ); + case iTIPPublish: + case iTIPRequest: + case iTIPRefresh: + case iTIPAdd: + { + if ( inc && inc->revision() > 0 && existingIncidence ) { + if ( inc->type() == "Todo" ) { + html += helper->makeLink( "reply", i18n( "[Record invitation into my to-do list]" ) ); + } else { + html += helper->makeLink( "reply", i18n( "[Record invitation into my calendar]" ) ); + } } - html += tdClose; - } - - if ( incidence && !existingIncidence ) { - // Accept - html += tdOpen; - //TODO: 4.4, remove the [] - html += helper->makeLink( "accept", i18nc( "accept to-do request", "[Accept]" ) ); - html += tdClose; - // Accept conditionally - html += tdOpen; - //TODO: 4.4, remove the [] - html += helper->makeLink( "accept_conditionally", - i18nc( "Accept conditionally", "[Accept cond.]" ) ); - html += tdClose; + if ( !myInc ) { + if ( rsvpReq ) { + // Accept + html += tdOpen; + html += helper->makeLink( "accept", + i18nc( "accept invitation", + "Accept" ) ); + html += tdClose; + + // Accept conditionally + html += tdOpen; + html += helper->makeLink( "accept_conditionally", + i18nc( "Accept invitation conditionally", + "Accept cond." ) ); + html += tdClose; + } - // Counter proposal - html += tdOpen; - //TODO: 4.4, remove the [] - html += helper->makeLink( "counter", i18n( "[Counter proposal]" ) ); - html += tdClose; + if ( rsvpReq ) { + // Counter proposal + html += tdOpen; + html += helper->makeLink( "counter", + i18nc( "invitation counter proposal", + "Counter proposal" ) ); + html += tdClose; + } - // Decline - html += tdOpen; - //TODO: 4.4, remove the [] - html += helper->makeLink( "decline", i18nc( "decline to-do request", "[Decline]" ) ); - html += tdClose; + if ( rsvpReq ) { + // Decline + html += tdOpen; + html += helper->makeLink( "decline", + i18nc( "decline invitation", + "Decline" ) ); + html += tdClose; + } - // Delegate - html += tdOpen; - //TODO: 4.4, remove the [] - html += helper->makeLink( "delegate", i18nc( "delegate to-do to another", "[Delegate]" ) ); - html += tdClose; + if ( !rsvpRec || ( inc && inc->revision() > 0 ) ) { + // Delegate + html += tdOpen; + html += helper->makeLink( "delegate", + i18nc( "delegate inviation to another", + "Delegate" ) ); + html += tdClose; + + // Forward + html += tdOpen; + html += helper->makeLink( "forward", + i18nc( "forward request to another", + "Forward" ) ); + html += tdClose; + + // Check calendar + if ( incBase->type() == "Event" ) { + html += tdOpen; + html += helper->makeLink( "check_calendar", + i18nc( "look for scheduling conflicts", + "Check my calendar" ) ); + html += tdClose; + } + } + } + break; + } - // Forward - html += tdOpen; - //TODO: 4.4, remove the [] - html += helper->makeLink( "forward", i18nc( "forward request to another", "[Forward]" ) ); - html += tdClose; + case iTIPCancel: + // Remove invitation + if ( existingIncidence ) { + html += tdOpen; + if ( inc->type() == "Todo" ) { + html += helper->makeLink( "cancel", + i18n( "Remove invitation from my task list" ) ); + } else { + html += helper->makeLink( "cancel", + i18n( "Remove invitation from my calendar" ) ); + } + html += tdClose; + } + break; - // Check in calendar - if ( incBase->type() == "Event" ) { + case iTIPReply: + { + // Record invitation response + Attendee *a = 0; + Attendee *ea = 0; + if ( inc ) { + a = inc->attendees().first(); + if ( a && helper->calendar() ) { + ea = findAttendee( existingIncidence, a->email() ); + } + } + if ( ea && ( ea->status() != Attendee::NeedsAction ) && ( ea->status() == a->status() ) ) { html += tdOpen; - //TODO: 4.4, remove the [] - //TODO: 4.4, change to "Check calendar" - html += helper->makeLink( "check_calendar", i18n("[Check my calendar]" ) ); + html += eventViewerAddTag( "i", i18n( "The response has already been recorded" ) ); html += tdClose; + } else { + if ( inc->type() == "Todo" ) { + html += helper->makeLink( "reply", i18n( "[Record response into my to-do list]" ) ); + } else { + html += helper->makeLink( "reply", i18n( "[Record response into my calendar]" ) ); + } } + break; } - break; - } - case iTIPCancel: - // Cancel event from my calendar - html += tdOpen; - //TODO: 4.4, remove the [] - html += helper->makeLink( "cancel", i18n( "[Remove this from my calendar]" ) ); - html += tdClose; - break; + case iTIPCounter: + // Counter proposal + html += tdOpen; + html += helper->makeLink( "accept_counter", i18n( "Accept" ) ); + html += tdClose; - case iTIPReply: - // Enter this into my calendar - html += tdOpen; - //TODO: 4.4, remove the [] - //TODO: 4.4, change string to "Enter this response into my..." - if ( incBase->type() == "Todo" ) { - html += helper->makeLink( "reply", i18n( "[Enter this into my to-do list]" ) ); - } else { - html += helper->makeLink( "reply", i18n( "[Enter this into my calendar]" ) ); - } - html += tdClose; - break; + html += tdOpen; + html += helper->makeLink( "decline_counter", i18n( "Decline" ) ); + html += tdClose; - case iTIPCounter: - html += tdOpen; - //TODO: 4.4, remove the [] - html += helper->makeLink( "accept_counter", i18n( "[Accept]" ) ); - html += tdClose; - - html += tdOpen; - //TODO: 4.4, remove the [] - html += helper->makeLink( "decline_counter", i18n( "[Decline]" ) ); - html += tdClose; - - html += tdOpen; - //TODO: 4.4, remove the [] - //TODO: 4.4, change string to "Check calendar" - html += helper->makeLink( "check_calendar", i18n( "[Check my calendar]" ) ); - html += tdClose; - break; + html += tdOpen; + html += helper->makeLink( "check_calendar", i18n( "Check my calendar" ) ); + html += tdClose; + break; - case iTIPDeclineCounter: - case iTIPNoMethod: - break; + case iTIPDeclineCounter: + case iTIPNoMethod: + break; } // close the groupware table html += "
      "; + const QString tdOpen = ""; const QString tdClose = "
      "; - // close the top-level table - html += "
      "; - kDebug() << html; + // Add the attendee list if I am the organizer + if ( myInc && helper->calendar() ) { + html += invitationAttendees( helper->calendar()->incidence( inc->uid() ) ); + } + + // close the top-level + html += ""; return html; } //@endcond -QString IncidenceFormatter::formatICalInvitation( QString invitation, Calendar *mCalendar, - InvitationFormatterHelper *helper ) +QString IncidenceFormatter::formatICalInvitation( QString invitation, + Calendar *mCalendar, + InvitationFormatterHelper *helper ) { - return formatICalInvitationHelper( invitation, mCalendar, helper, false ); + return formatICalInvitationHelper( invitation, mCalendar, helper, false, + KSystemTimeZones::local() ); } -QString IncidenceFormatter::formatICalInvitationNoHtml( QString invitation, Calendar *mCalendar, - InvitationFormatterHelper *helper ) +QString IncidenceFormatter::formatICalInvitationNoHtml( QString invitation, + Calendar *mCalendar, + InvitationFormatterHelper *helper ) { - return formatICalInvitationHelper( invitation, mCalendar, helper, true ); + return formatICalInvitationHelper( invitation, mCalendar, helper, true, + KSystemTimeZones::local() ); } /******************************************************************* * Helper functions for the Incidence tooltips *******************************************************************/ //@cond PRIVATE class KCal::IncidenceFormatter::ToolTipVisitor : public IncidenceBase::Visitor { public: ToolTipVisitor() : mRichText( true ), mSpec( KDateTime::Spec() ), mResult( "" ) {} bool act( IncidenceBase *incidence, bool richText=true, KDateTime::Spec spec=KDateTime::Spec() ) { mRichText = richText; mSpec = spec; mResult = ""; return incidence ? incidence->accept( *this ) : false; } QString result() const { return mResult; } protected: bool visit( Event *event ); bool visit( Todo *todo ); bool visit( Journal *journal ); bool visit( FreeBusy *fb ); QString dateRangeText( Event *event ); QString dateRangeText( Todo *todo ); QString dateRangeText( Journal *journal ); QString dateRangeText( FreeBusy *fb ); QString generateToolTip( Incidence *incidence, QString dtRangeText ); protected: bool mRichText; KDateTime::Spec mSpec; QString mResult; }; QString IncidenceFormatter::ToolTipVisitor::dateRangeText( Event *event ) { //FIXME: support mRichText==false QString ret; QString tmp; if ( event->isMultiDay() ) { tmp = IncidenceFormatter::dateToString( event->dtStart(), true, mSpec ); ret += "
      " + i18nc( "Event start", "From: %1", tmp ); tmp = IncidenceFormatter::dateToString( event->dtEnd(), true, mSpec ); ret += "
      " + i18nc( "Event end","To: %1", tmp ); } else { ret += "
      " + - i18n( "Date: %1", IncidenceFormatter::dateToString( event->dtStart(), true, mSpec ) ); + i18n( "Date: %1", + IncidenceFormatter::dateToString( event->dtStart(), true, mSpec ) ); if ( !event->allDay() ) { const QString dtStartTime = IncidenceFormatter::timeToString( event->dtStart(), true, mSpec ); const QString dtEndTime = IncidenceFormatter::timeToString( event->dtEnd(), true, mSpec ); if ( dtStartTime == dtEndTime ) { // to prevent 'Time: 17:00 - 17:00' tmp = "
      " + i18nc( "time for event", "Time: %1", dtStartTime ); } else { tmp = "
      " + i18nc( "time range for event", "Time: %1 - %2", dtStartTime, dtEndTime ); } ret += tmp; } } return ret.replace( ' ', " " ); } QString IncidenceFormatter::ToolTipVisitor::dateRangeText( Todo *todo ) { //FIXME: support mRichText==false QString ret; if ( todo->hasStartDate() && todo->dtStart().isValid() ) { // No need to add here. This is separated issue and each line // is very visible on its own. On the other hand... Yes, I like it // italics here :) ret += "
      " + i18n( "Start: %1", IncidenceFormatter::dateToString( todo->dtStart( false ), true, mSpec ) ); } if ( todo->hasDueDate() && todo->dtDue().isValid() ) { ret += "
      " + i18n( "Due: %1", IncidenceFormatter::dateTimeToString( todo->dtDue(), todo->allDay(), true, mSpec ) ); } if ( todo->isCompleted() ) { ret += "
      " + i18n( "Completed: %1", todo->completedStr() ); } else { ret += "
      " + i18nc( "percent complete", "%1 % completed", todo->percentComplete() ); } return ret.replace( ' ', " " ); } QString IncidenceFormatter::ToolTipVisitor::dateRangeText( Journal *journal ) { //FIXME: support mRichText==false QString ret; if ( journal->dtStart().isValid() ) { ret += "
      " + i18n( "Date: %1", IncidenceFormatter::dateToString( journal->dtStart(), false, mSpec ) ); } return ret.replace( ' ', " " ); } QString IncidenceFormatter::ToolTipVisitor::dateRangeText( FreeBusy *fb ) { //FIXME: support mRichText==false QString ret; ret = "
      " + i18n( "Period start: %1", KGlobal::locale()->formatDateTime( fb->dtStart().dateTime() ) ); ret += "
      " + i18n( "Period start: %1", KGlobal::locale()->formatDateTime( fb->dtEnd().dateTime() ) ); return ret.replace( ' ', " " ); } bool IncidenceFormatter::ToolTipVisitor::visit( Event *event ) { mResult = generateToolTip( event, dateRangeText( event ) ); return !mResult.isEmpty(); } bool IncidenceFormatter::ToolTipVisitor::visit( Todo *todo ) { mResult = generateToolTip( todo, dateRangeText( todo ) ); return !mResult.isEmpty(); } bool IncidenceFormatter::ToolTipVisitor::visit( Journal *journal ) { mResult = generateToolTip( journal, dateRangeText( journal ) ); return !mResult.isEmpty(); } bool IncidenceFormatter::ToolTipVisitor::visit( FreeBusy *fb ) { //FIXME: support mRichText==false mResult = "" + i18n( "Free/Busy information for %1", fb->organizer().fullName() ) + ""; mResult += dateRangeText( fb ); mResult += ""; return !mResult.isEmpty(); } QString IncidenceFormatter::ToolTipVisitor::generateToolTip( Incidence *incidence, QString dtRangeText ) { //FIXME: support mRichText==false if ( !incidence ) { return QString(); } QString tmp = ""+ incidence->richSummary() + ""; tmp += dtRangeText; if ( !incidence->location().isEmpty() ) { // Put Location: in italics tmp += "
      " + - i18n( "Location: %1", incidence->richLocation() ); + i18n( "Location: %1", incidence->richLocation() ); } if ( !incidence->description().isEmpty() ) { QString desc( incidence->description() ); if ( !incidence->descriptionIsRich() ) { if ( desc.length() > 120 ) { desc = desc.left( 120 ) + "..."; } desc = Qt::escape( desc ).replace( '\n', "
      " ); } else { // TODO: truncate the description when it's rich text } tmp += "
      ----------
      " + i18n( "Description:" ) + "
      " + desc; } tmp += "
      "; return tmp; } //@endcond QString IncidenceFormatter::toolTipString( IncidenceBase *incidence, bool richText ) { return toolTipStr( incidence, richText, KDateTime::Spec() ); } QString IncidenceFormatter::toolTipStr( IncidenceBase *incidence, bool richText, KDateTime::Spec spec ) { ToolTipVisitor v; if ( v.act( incidence, richText, spec ) ) { return v.result(); } else { return QString(); } } /******************************************************************* * Helper functions for the Incidence tooltips *******************************************************************/ //@cond PRIVATE static QString mailBodyIncidence( Incidence *incidence ) { QString body; if ( !incidence->summary().isEmpty() ) { body += i18n( "Summary: %1\n", incidence->richSummary() ); } if ( !incidence->organizer().isEmpty() ) { body += i18n( "Organizer: %1\n", incidence->organizer().fullName() ); } if ( !incidence->location().isEmpty() ) { body += i18n( "Location: %1\n", incidence->richLocation() ); } return body; } //@endcond //@cond PRIVATE class KCal::IncidenceFormatter::MailBodyVisitor : public IncidenceBase::Visitor { public: MailBodyVisitor() : mSpec( KDateTime::Spec() ), mResult( "" ) {} bool act( IncidenceBase *incidence, KDateTime::Spec spec=KDateTime::Spec() ) { mSpec = spec; mResult = ""; return incidence ? incidence->accept( *this ) : false; } QString result() const { return mResult; } protected: bool visit( Event *event ); bool visit( Todo *todo ); bool visit( Journal *journal ); bool visit( FreeBusy * ) { mResult = i18n( "This is a Free Busy Object" ); return !mResult.isEmpty(); } protected: KDateTime::Spec mSpec; QString mResult; }; bool IncidenceFormatter::MailBodyVisitor::visit( Event *event ) { QString recurrence[]= { i18nc( "no recurrence", "None" ), i18nc( "event recurs by minutes", "Minutely" ), i18nc( "event recurs by hours", "Hourly" ), i18nc( "event recurs by days", "Daily" ), i18nc( "event recurs by weeks", "Weekly" ), i18nc( "event recurs same position (e.g. first monday) each month", "Monthly Same Position" ), i18nc( "event recurs same day each month", "Monthly Same Day" ), i18nc( "event recurs same month each year", "Yearly Same Month" ), i18nc( "event recurs same day each year", "Yearly Same Day" ), i18nc( "event recurs same position (e.g. first monday) each year", "Yearly Same Position" ) }; mResult = mailBodyIncidence( event ); mResult += i18n( "Start Date: %1\n", IncidenceFormatter::dateToString( event->dtStart(), true, mSpec ) ); if ( !event->allDay() ) { mResult += i18n( "Start Time: %1\n", IncidenceFormatter::timeToString( event->dtStart(), true, mSpec ) ); } if ( event->dtStart() != event->dtEnd() ) { mResult += i18n( "End Date: %1\n", IncidenceFormatter::dateToString( event->dtEnd(), true, mSpec ) ); } if ( !event->allDay() ) { mResult += i18n( "End Time: %1\n", IncidenceFormatter::timeToString( event->dtEnd(), true, mSpec ) ); } if ( event->recurs() ) { Recurrence *recur = event->recurrence(); // TODO: Merge these two to one of the form "Recurs every 3 days" mResult += i18n( "Recurs: %1\n", recurrence[ recur->recurrenceType() ] ); mResult += i18n( "Frequency: %1\n", event->recurrence()->frequency() ); if ( recur->duration() > 0 ) { mResult += i18np( "Repeats once", "Repeats %1 times", recur->duration() ); mResult += '\n'; } else { if ( recur->duration() != -1 ) { // TODO_Recurrence: What to do with all-day QString endstr; if ( event->allDay() ) { endstr = KGlobal::locale()->formatDate( recur->endDate() ); } else { endstr = KGlobal::locale()->formatDateTime( recur->endDateTime().dateTime() ); } mResult += i18n( "Repeat until: %1\n", endstr ); } else { mResult += i18n( "Repeats forever\n" ); } } } QString details = event->richDescription(); if ( !details.isEmpty() ) { mResult += i18n( "Details:\n%1\n", details ); } return !mResult.isEmpty(); } bool IncidenceFormatter::MailBodyVisitor::visit( Todo *todo ) { mResult = mailBodyIncidence( todo ); if ( todo->hasStartDate() && todo->dtStart().isValid() ) { mResult += i18n( "Start Date: %1\n", IncidenceFormatter::dateToString( todo->dtStart(false), true, mSpec ) ); if ( !todo->allDay() ) { mResult += i18n( "Start Time: %1\n", IncidenceFormatter::timeToString( todo->dtStart(false), true, mSpec ) ); } } if ( todo->hasDueDate() && todo->dtDue().isValid() ) { mResult += i18n( "Due Date: %1\n", IncidenceFormatter::dateToString( todo->dtDue(), true, mSpec ) ); if ( !todo->allDay() ) { mResult += i18n( "Due Time: %1\n", IncidenceFormatter::timeToString( todo->dtDue(), true, mSpec ) ); } } QString details = todo->richDescription(); if ( !details.isEmpty() ) { mResult += i18n( "Details:\n%1\n", details ); } return !mResult.isEmpty(); } bool IncidenceFormatter::MailBodyVisitor::visit( Journal *journal ) { mResult = mailBodyIncidence( journal ); - mResult += i18n( "Date: %1\n", IncidenceFormatter::dateToString( journal->dtStart(), true, - mSpec ) ); - + mResult += i18n( "Date: %1\n", + IncidenceFormatter::dateToString( journal->dtStart(), true, mSpec ) ); if ( !journal->allDay() ) { mResult += i18n( "Time: %1\n", IncidenceFormatter::timeToString( journal->dtStart(), true, mSpec ) ); - } if ( !journal->description().isEmpty() ) { mResult += i18n( "Text of the journal:\n%1\n", journal->richDescription() ); } return !mResult.isEmpty(); } //@endcond QString IncidenceFormatter::mailBodyString( IncidenceBase *incidence ) { return mailBodyStr( incidence, KDateTime::Spec() ); } QString IncidenceFormatter::mailBodyStr( IncidenceBase *incidence, KDateTime::Spec spec ) { if ( !incidence ) { return QString(); } MailBodyVisitor v; if ( v.act( incidence, spec ) ) { return v.result(); } return QString(); } //@cond PRIVATE static QString recurEnd( Incidence *incidence ) { QString endstr; if ( incidence->allDay() ) { endstr = KGlobal::locale()->formatDate( incidence->recurrence()->endDate() ); } else { endstr = KGlobal::locale()->formatDateTime( incidence->recurrence()->endDateTime() ); } return endstr; } //@endcond QString IncidenceFormatter::recurrenceString( Incidence *incidence ) { if ( !incidence->recurs() ) { return i18n( "No recurrence" ); } QStringList dayList; dayList.append( i18n( "31st Last" ) ); dayList.append( i18n( "30th Last" ) ); dayList.append( i18n( "29th Last" ) ); dayList.append( i18n( "28th Last" ) ); dayList.append( i18n( "27th Last" ) ); dayList.append( i18n( "26th Last" ) ); dayList.append( i18n( "25th Last" ) ); dayList.append( i18n( "24th Last" ) ); dayList.append( i18n( "23rd Last" ) ); dayList.append( i18n( "22nd Last" ) ); dayList.append( i18n( "21st Last" ) ); dayList.append( i18n( "20th Last" ) ); dayList.append( i18n( "19th Last" ) ); dayList.append( i18n( "18th Last" ) ); dayList.append( i18n( "17th Last" ) ); dayList.append( i18n( "16th Last" ) ); dayList.append( i18n( "15th Last" ) ); dayList.append( i18n( "14th Last" ) ); dayList.append( i18n( "13th Last" ) ); dayList.append( i18n( "12th Last" ) ); dayList.append( i18n( "11th Last" ) ); dayList.append( i18n( "10th Last" ) ); dayList.append( i18n( "9th Last" ) ); dayList.append( i18n( "8th Last" ) ); dayList.append( i18n( "7th Last" ) ); dayList.append( i18n( "6th Last" ) ); dayList.append( i18n( "5th Last" ) ); dayList.append( i18n( "4th Last" ) ); dayList.append( i18n( "3rd Last" ) ); dayList.append( i18n( "2nd Last" ) ); dayList.append( i18nc( "last day of the month", "Last" ) ); dayList.append( i18nc( "unknown day of the month", "unknown" ) ); //#31 - zero offset from UI dayList.append( i18n( "1st" ) ); dayList.append( i18n( "2nd" ) ); dayList.append( i18n( "3rd" ) ); dayList.append( i18n( "4th" ) ); dayList.append( i18n( "5th" ) ); dayList.append( i18n( "6th" ) ); dayList.append( i18n( "7th" ) ); dayList.append( i18n( "8th" ) ); dayList.append( i18n( "9th" ) ); dayList.append( i18n( "10th" ) ); dayList.append( i18n( "11th" ) ); dayList.append( i18n( "12th" ) ); dayList.append( i18n( "13th" ) ); dayList.append( i18n( "14th" ) ); dayList.append( i18n( "15th" ) ); dayList.append( i18n( "16th" ) ); dayList.append( i18n( "17th" ) ); dayList.append( i18n( "18th" ) ); dayList.append( i18n( "19th" ) ); dayList.append( i18n( "20th" ) ); dayList.append( i18n( "21st" ) ); dayList.append( i18n( "22nd" ) ); dayList.append( i18n( "23rd" ) ); dayList.append( i18n( "24th" ) ); dayList.append( i18n( "25th" ) ); dayList.append( i18n( "26th" ) ); dayList.append( i18n( "27th" ) ); dayList.append( i18n( "28th" ) ); dayList.append( i18n( "29th" ) ); dayList.append( i18n( "30th" ) ); dayList.append( i18n( "31st" ) ); int weekStart = KGlobal::locale()->weekStartDay(); QString dayNames; QString txt; const KCalendarSystem *calSys = KGlobal::locale()->calendar(); Recurrence *recur = incidence->recurrence(); switch ( recur->recurrenceType() ) { case Recurrence::rNone: return i18n( "No recurrence" ); case Recurrence::rMinutely: if ( recur->duration() != -1 ) { txt = i18np( "Recurs every minute until %2", "Recurs every %1 minutes until %2", recur->frequency(), recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18np( "Recurs every minute", "Recurs every %1 minutes", recur->frequency() ); case Recurrence::rHourly: if ( recur->duration() != -1 ) { txt = i18np( "Recurs hourly until %2", "Recurs every %1 hours until %2", recur->frequency(), recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18np( "Recurs hourly", "Recurs every %1 hours", recur->frequency() ); case Recurrence::rDaily: if ( recur->duration() != -1 ) { txt = i18np( "Recurs daily until %2", "Recurs every %1 days until %2", recur->frequency(), recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18np( "Recurs daily", "Recurs every %1 days", recur->frequency() ); case Recurrence::rWeekly: { bool addSpace = false; for ( int i = 0; i < 7; ++i ) { if ( recur->days().testBit( ( i + weekStart + 6 ) % 7 ) ) { if ( addSpace ) { dayNames.append( i18nc( "separator for list of days", ", " ) ); } dayNames.append( calSys->weekDayName( ( ( i + weekStart + 6 ) % 7 ) + 1, KCalendarSystem::ShortDayName ) ); addSpace = true; } } if ( dayNames.isEmpty() ) { dayNames = i18nc( "Recurs weekly on no days", "no days" ); } if ( recur->duration() != -1 ) { txt = i18ncp( "Recurs weekly on [list of days] until end-date", "Recurs weekly on %2 until %3", "Recurs every %1 weeks on %2 until %3", recur->frequency(), dayNames, recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18ncp( "Recurs weekly on [list of days]", "Recurs weekly on %2", "Recurs every %1 weeks on %2", recur->frequency(), dayNames ); } case Recurrence::rMonthlyPos: { KCal::RecurrenceRule::WDayPos rule = recur->monthPositions()[0]; if ( recur->duration() != -1 ) { txt = i18ncp( "Recurs every N months on the [2nd|3rd|...]" " weekdayname until end-date", "Recurs every month on the %2 %3 until %4", "Recurs every %1 months on the %2 %3 until %4", recur->frequency(), dayList[rule.pos() + 31], calSys->weekDayName( rule.day(),KCalendarSystem::LongDayName ), recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18ncp( "Recurs every N months on the [2nd|3rd|...] weekdayname", "Recurs every month on the %2 %3", "Recurs every %1 months on the %2 %3", recur->frequency(), dayList[rule.pos() + 31], calSys->weekDayName( rule.day(), KCalendarSystem::LongDayName ) ); } case Recurrence::rMonthlyDay: { int days = recur->monthDays()[0]; if ( recur->duration() != -1 ) { txt = i18ncp( "Recurs monthly on the [1st|2nd|...] day until end-date", "Recurs monthly on the %2 day until %3", "Recurs every %1 months on the %2 day until %3", recur->frequency(), dayList[days + 31], recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18ncp( "Recurs monthly on the [1st|2nd|...] day", "Recurs monthly on the %2 day", "Recurs every %1 month on the %2 day", recur->frequency(), dayList[days + 31] ); } case Recurrence::rYearlyMonth: { if ( recur->duration() != -1 ) { txt = i18ncp( "Recurs Every N years on month-name [1st|2nd|...]" " until end-date", "Recurs yearly on %2 %3 until %4", "Recurs every %1 years on %2 %3 until %4", recur->frequency(), calSys->monthName( recur->yearMonths()[0], recur->startDate().year() ), dayList[ recur->yearDates()[0] + 31 ], recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } if ( !recur->yearDates().isEmpty() ) { return i18ncp( "Recurs Every N years on month-name [1st|2nd|...]", "Recurs yearly on %2 %3", "Recurs every %1 years on %2 %3", recur->frequency(), calSys->monthName( recur->yearMonths()[0], recur->startDate().year() ), dayList[ recur->yearDates()[0] + 31 ] ); } else { if (!recur->yearMonths().isEmpty() ) { return i18nc( "Recurs Every year on month-name [1st|2nd|...]", "Recurs yearly on %1 %2", calSys->monthName( recur->yearMonths()[0], recur->startDate().year() ), dayList[ recur->startDate().day() + 31 ] ); } else { return i18nc( "Recurs Every year on month-name [1st|2nd|...]", "Recurs yearly on %1 %2", calSys->monthName( recur->startDate().month(), recur->startDate().year() ), dayList[ recur->startDate().day() + 31 ] ); } } } case Recurrence::rYearlyDay: if ( recur->duration() != -1 ) { txt = i18ncp( "Recurs every N years on day N until end-date", "Recurs every year on day %2 until %3", "Recurs every %1 years" " on day %2 until %3", recur->frequency(), recur->yearDays()[0], recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18ncp( "Recurs every N YEAR[S] on day N", "Recurs every year on day %2", "Recurs every %1 years" " on day %2", recur->frequency(), recur->yearDays()[0] ); case Recurrence::rYearlyPos: { KCal::RecurrenceRule::WDayPos rule = recur->yearPositions()[0]; if ( recur->duration() != -1 ) { txt = i18ncp( "Every N years on the [2nd|3rd|...] weekdayname " "of monthname until end-date", "Every year on the %2 %3 of %4 until %5", "Every %1 years on the %2 %3 of %4" " until %5", recur->frequency(), dayList[rule.pos() + 31], calSys->weekDayName( rule.day(), KCalendarSystem::LongDayName ), calSys->monthName( recur->yearMonths()[0], recur->startDate().year() ), recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18ncp( "Every N years on the [2nd|3rd|...] weekdayname " "of monthname", "Every year on the %2 %3 of %4", "Every %1 years on the %2 %3 of %4", recur->frequency(), dayList[rule.pos() + 31], calSys->weekDayName( rule.day(), KCalendarSystem::LongDayName ), calSys->monthName( recur->yearMonths()[0], recur->startDate().year() ) ); } default: return i18n( "Incidence recurs" ); } } QString IncidenceFormatter::timeToString( const KDateTime &date, bool shortfmt, const KDateTime::Spec &spec ) { if ( spec.isValid() ) { QString timeZone; if ( spec.timeZone() != KSystemTimeZones::local() ) { timeZone = ' ' + spec.timeZone().name(); } return KGlobal::locale()->formatTime( date.toTimeSpec( spec ).time(), !shortfmt ) + timeZone; } else { return KGlobal::locale()->formatTime( date.time(), !shortfmt ); } } QString IncidenceFormatter::dateToString( const KDateTime &date, bool shortfmt, const KDateTime::Spec &spec ) { if ( spec.isValid() ) { QString timeZone; if ( spec.timeZone() != KSystemTimeZones::local() ) { timeZone = ' ' + spec.timeZone().name(); } return KGlobal::locale()->formatDate( date.toTimeSpec( spec ).date(), ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ) + timeZone; } else { return KGlobal::locale()->formatDate( date.date(), ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ); } } QString IncidenceFormatter::dateTimeToString( const KDateTime &date, bool allDay, bool shortfmt, const KDateTime::Spec &spec ) { if ( allDay ) { return dateToString( date, shortfmt, spec ); } if ( spec.isValid() ) { QString timeZone; if ( spec.timeZone() != KSystemTimeZones::local() ) { timeZone = ' ' + spec.timeZone().name(); } return KGlobal::locale()->formatDateTime( date.toTimeSpec( spec ).dateTime(), ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ) + timeZone; } else { return KGlobal::locale()->formatDateTime( date.dateTime(), ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ); } } diff --git a/kcal/recurrencerule.cpp b/kcal/recurrencerule.cpp index cd50b4fb9..e831d10f8 100644 --- a/kcal/recurrencerule.cpp +++ b/kcal/recurrencerule.cpp @@ -1,2204 +1,2206 @@ /* This file is part of libkcal. Copyright (c) 2005 Reinhold Kainhofer Copyright (c) 2006-2008 David Jarvie This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "recurrencerule.h" #include #include #include #include #include #include #include using namespace KCal; // Maximum number of intervals to process const int LOOP_LIMIT = 10000; static QString dumpTime( const KDateTime &dt ); // for debugging /*========================================================================= = = = IMPORTANT CODING NOTE: = = = = Recurrence handling code is time critical, especially for sub-daily = = recurrences. For example, if getNextDate() is called repeatedly to = = check all consecutive occurrences over a few years, on a slow machine = = this could take many seconds to complete in the worst case. Simple = = sub-daily recurrences are optimised by use of mTimedRepetition. = = = ==========================================================================*/ /************************************************************************** * DateHelper * **************************************************************************/ //@cond PRIVATE class DateHelper { public: #ifndef NDEBUG static QString dayName( short day ); #endif static QDate getNthWeek( int year, int weeknumber, short weekstart = 1 ); static int weekNumbersInYear( int year, short weekstart = 1 ); static int getWeekNumber( const QDate &date, short weekstart, int *year = 0 ); static int getWeekNumberNeg( const QDate &date, short weekstart, int *year = 0 ); // Convert to QDate, allowing for day < 0. // month and day must be non-zero. static QDate getDate( int year, int month, int day ) { if ( day >= 0 ) { return QDate( year, month, day ); } else { if ( ++month > 12 ) { month = 1; ++year; } return QDate( year, month, 1 ).addDays( day ); } } }; #ifndef NDEBUG // TODO: Move to a general library / class, as we need the same in the iCal // generator and in the xcal format QString DateHelper::dayName( short day ) { switch ( day ) { case 1: return "MO"; case 2: return "TU"; case 3: return "WE"; case 4: return "TH"; case 5: return "FR"; case 6: return "SA"; case 7: return "SU"; default: return "??"; } } #endif QDate DateHelper::getNthWeek( int year, int weeknumber, short weekstart ) { if ( weeknumber == 0 ) { return QDate(); } // Adjust this to the first day of week #1 of the year and add 7*weekno days. QDate dt( year, 1, 4 ); // Week #1 is the week that contains Jan 4 int adjust = -( 7 + dt.dayOfWeek() - weekstart ) % 7; if ( weeknumber > 0 ) { dt = dt.addDays( 7 * (weeknumber-1) + adjust ); } else if ( weeknumber < 0 ) { dt = dt.addYears( 1 ); dt = dt.addDays( 7 * weeknumber + adjust ); } return dt; } int DateHelper::getWeekNumber( const QDate &date, short weekstart, int *year ) { int y = date.year(); QDate dt( y, 1, 4 ); // <= definitely in week #1 dt = dt.addDays( -( 7 + dt.dayOfWeek() - weekstart ) % 7 ); // begin of week #1 int daysto = dt.daysTo( date ); if ( daysto < 0 ) { // in first week of year --y; dt = QDate( y, 1, 4 ); dt = dt.addDays( -( 7 + dt.dayOfWeek() - weekstart ) % 7 ); // begin of week #1 daysto = dt.daysTo( date ); } else if ( daysto > 355 ) { // near the end of the year - check if it's next year QDate dtn( y+1, 1, 4 ); // <= definitely first week of next year dtn = dtn.addDays( -( 7 + dtn.dayOfWeek() - weekstart ) % 7 ); int dayston = dtn.daysTo( date ); if ( dayston >= 0 ) { // in first week of next year; ++y; daysto = dayston; } } if ( year ) { *year = y; } return daysto / 7 + 1; } int DateHelper::weekNumbersInYear( int year, short weekstart ) { QDate dt( year, 1, weekstart ); QDate dt1( year + 1, 1, weekstart ); return dt.daysTo( dt1 ) / 7; } // Week number from the end of the year int DateHelper::getWeekNumberNeg( const QDate &date, short weekstart, int *year ) { int weekpos = getWeekNumber( date, weekstart, year ); return weekNumbersInYear( *year, weekstart ) - weekpos - 1; } //@endcond /************************************************************************** * Constraint * **************************************************************************/ //@cond PRIVATE class Constraint { public: typedef QList List; explicit Constraint( KDateTime::Spec, int wkst = 1 ); Constraint( const KDateTime &dt, RecurrenceRule::PeriodType type, int wkst ); void clear(); void setYear( int n ) { year = n; useCachedDt = false; } void setMonth( int n ) { month = n; useCachedDt = false; } void setDay( int n ) { day = n; useCachedDt = false; } void setHour( int n ) { hour = n; useCachedDt = false; } void setMinute( int n ) { minute = n; useCachedDt = false; } void setSecond( int n ) { second = n; useCachedDt = false; } void setWeekday( int n ) { weekday = n; useCachedDt = false; } void setWeekdaynr( int n ) { weekdaynr = n; useCachedDt = false; } void setWeeknumber( int n ) { weeknumber = n; useCachedDt = false; } void setYearday( int n ) { yearday = n; useCachedDt = false; } void setWeekstart( int n ) { weekstart = n; useCachedDt = false; } void setSecondOccurrence( int n ) { secondOccurrence = n; useCachedDt = false; } int year; // 0 means unspecified int month; // 0 means unspecified int day; // 0 means unspecified int hour; // -1 means unspecified int minute; // -1 means unspecified int second; // -1 means unspecified int weekday; // 0 means unspecified int weekdaynr; // index of weekday in month/year (0=unspecified) int weeknumber; // 0 means unspecified int yearday; // 0 means unspecified int weekstart; // first day of week (1=monday, 7=sunday, 0=unspec.) KDateTime::Spec timespec; // time zone etc. to use bool secondOccurrence; // the time is the second occurrence during daylight savings shift bool readDateTime( const KDateTime &dt, RecurrenceRule::PeriodType type ); bool matches( const QDate &dt, RecurrenceRule::PeriodType type ) const; bool matches( const KDateTime &dt, RecurrenceRule::PeriodType type ) const; bool merge( const Constraint &interval ); bool isConsistent() const; bool isConsistent( RecurrenceRule::PeriodType period ) const; bool increase( RecurrenceRule::PeriodType type, int freq ); KDateTime intervalDateTime( RecurrenceRule::PeriodType type ) const; QList dateTimes( RecurrenceRule::PeriodType type ) const; void appendDateTime( const QDate &date, const QTime &time, QList &list ) const; void dump() const; private: mutable bool useCachedDt; mutable KDateTime cachedDt; }; Constraint::Constraint( KDateTime::Spec spec, int wkst ) : weekstart( wkst ), timespec( spec ) { clear(); } Constraint::Constraint( const KDateTime &dt, RecurrenceRule::PeriodType type, int wkst ) : weekstart( wkst ), timespec( dt.timeSpec() ) { clear(); readDateTime( dt, type ); } void Constraint::clear() { year = 0; month = 0; day = 0; hour = -1; minute = -1; second = -1; weekday = 0; weekdaynr = 0; weeknumber = 0; yearday = 0; secondOccurrence = false; useCachedDt = false; } bool Constraint::matches( const QDate &dt, RecurrenceRule::PeriodType type ) const { // If the event recurs in week 53 or 1, the day might not belong to the same // year as the week it is in. E.g. Jan 1, 2005 is in week 53 of year 2004. // So we can't simply check the year in that case! if ( weeknumber == 0 ) { if ( year > 0 && year != dt.year() ) { return false; } } else { int y; if ( weeknumber > 0 && weeknumber != DateHelper::getWeekNumber( dt, weekstart, &y ) ) { return false; } if ( weeknumber < 0 && weeknumber != DateHelper::getWeekNumberNeg( dt, weekstart, &y ) ) { return false; } if ( year > 0 && year != y ) { return false; } } if ( month > 0 && month != dt.month() ) { return false; } if ( day > 0 && day != dt.day() ) { return false; } if ( day < 0 && dt.day() != ( dt.daysInMonth() + day + 1 ) ) { return false; } if ( weekday > 0 ) { if ( weekday != dt.dayOfWeek() ) { return false; } if ( weekdaynr != 0 ) { // If it's a yearly recurrence and a month is given, the position is // still in the month, not in the year. if ( ( type == RecurrenceRule::rMonthly ) || ( type == RecurrenceRule::rYearly && month > 0 ) ) { // Monthly if ( weekdaynr > 0 && weekdaynr != ( dt.day() - 1 ) / 7 + 1 ) { return false; } if ( weekdaynr < 0 && weekdaynr != -( ( dt.daysInMonth() - dt.day() ) / 7 + 1 ) ) { return false; } } else { // Yearly if ( weekdaynr > 0 && weekdaynr != ( dt.dayOfYear() - 1 ) / 7 + 1 ) { return false; } if ( weekdaynr < 0 && weekdaynr != -( ( dt.daysInYear() - dt.dayOfYear() ) / 7 + 1 ) ) { return false; } } } } if ( yearday > 0 && yearday != dt.dayOfYear() ) { return false; } if ( yearday < 0 && yearday != dt.daysInYear() - dt.dayOfYear() + 1 ) { return false; } return true; } /* Check for a match with the specified date/time. * The date/time's time specification must correspond with that of the start date/time. */ bool Constraint::matches( const KDateTime &dt, RecurrenceRule::PeriodType type ) const { if ( ( hour >= 0 && ( hour != dt.time().hour() || secondOccurrence != dt.isSecondOccurrence() ) ) || ( minute >= 0 && minute != dt.time().minute() ) || ( second >= 0 && second != dt.time().second() ) || !matches( dt.date(), type ) ) { return false; } return true; } bool Constraint::isConsistent( RecurrenceRule::PeriodType /*period*/) const { // TODO: Check for consistency, e.g. byyearday=3 and bymonth=10 return true; } // Return a date/time set to the constraint values, but with those parts less // significant than the given period type set to 1 (for dates) or 0 (for times). KDateTime Constraint::intervalDateTime( RecurrenceRule::PeriodType type ) const { if ( useCachedDt ) { return cachedDt; } QDate d; QTime t( 0, 0, 0 ); bool subdaily = true; switch ( type ) { case RecurrenceRule::rSecondly: t.setHMS( hour, minute, second ); break; case RecurrenceRule::rMinutely: t.setHMS( hour, minute, 0 ); break; case RecurrenceRule::rHourly: t.setHMS( hour, 0, 0 ); break; case RecurrenceRule::rDaily: break; case RecurrenceRule::rWeekly: d = DateHelper::getNthWeek( year, weeknumber, weekstart ); subdaily = false; break; case RecurrenceRule::rMonthly: d.setYMD( year, month, 1 ); subdaily = false; break; case RecurrenceRule::rYearly: d.setYMD( year, 1, 1 ); subdaily = false; break; default: break; } if ( subdaily ) { d = DateHelper::getDate( year, (month>0)?month:1, day?day:1 ); } cachedDt = KDateTime( d, t, timespec ); if ( secondOccurrence ) { cachedDt.setSecondOccurrence( true ); } useCachedDt = true; return cachedDt; } bool Constraint::merge( const Constraint &interval ) { #define mergeConstraint( name, cmparison ) \ if ( interval.name cmparison ) { \ if ( !( name cmparison ) ) { \ name = interval.name; \ } else if ( name != interval.name ) { \ return false;\ } \ } useCachedDt = false; mergeConstraint( year, > 0 ); mergeConstraint( month, > 0 ); mergeConstraint( day, != 0 ); mergeConstraint( hour, >= 0 ); mergeConstraint( minute, >= 0 ); mergeConstraint( second, >= 0 ); mergeConstraint( weekday, != 0 ); mergeConstraint( weekdaynr, != 0 ); mergeConstraint( weeknumber, != 0 ); mergeConstraint( yearday, != 0 ); #undef mergeConstraint return true; } // Y M D | H Mn S | WD #WD | WN | YD // required: // x | x x x | | | // 0) Trivial: Exact date given, maybe other restrictions // x x x | x x x | | | // 1) Easy case: no weekly restrictions -> at most a loop through possible dates // x + + | x x x | - - | - | - // 2) Year day is given -> date known // x | x x x | | | + // 3) week number is given -> loop through all days of that week. Further // restrictions will be applied in the end, when we check all dates for // consistency with the constraints // x | x x x | | + | (-) // 4) week day is specified -> // x | x x x | x ? | (-)| (-) // 5) All possiblecases have already been treated, so this must be an error! QList Constraint::dateTimes( RecurrenceRule::PeriodType type ) const { QList result; bool done = false; if ( !isConsistent( type ) ) { return result; } // TODO_Recurrence: Handle all-day QTime tm( hour, minute, second ); if ( !done && day && month > 0 ) { appendDateTime( DateHelper::getDate( year, month, day ), tm, result ); done = true; } if ( !done && weekday == 0 && weeknumber == 0 && yearday == 0 ) { // Easy case: date is given, not restrictions by week or yearday uint mstart = ( month > 0 ) ? month : 1; uint mend = ( month <= 0 ) ? 12 : month; for ( uint m = mstart; m <= mend; ++m ) { uint dstart, dend; if ( day > 0 ) { dstart = dend = day; } else if ( day < 0 ) { QDate date( year, month, 1 ); dstart = dend = date.daysInMonth() + day + 1; } else { QDate date( year, month, 1 ); dstart = 1; dend = date.daysInMonth(); } uint d = dstart; for ( QDate dt( year, m, dstart ); ; dt = dt.addDays( 1 ) ) { appendDateTime( dt, tm, result ); if ( ++d > dend ) { break; } } } done = true; } // Else: At least one of the week / yearday restrictions was given... // If we have a yearday (and of course a year), we know the exact date if ( !done && yearday != 0 ) { // yearday < 0 means from end of year, so we'll need Jan 1 of the next year QDate d( year + ( ( yearday > 0 ) ? 0 : 1 ), 1, 1 ); d = d.addDays( yearday - ( ( yearday > 0 ) ? 1 : 0 ) ); appendDateTime( d, tm, result ); done = true; } // Else: If we have a weeknumber, we have at most 7 possible dates, loop through them if ( !done && weeknumber != 0 ) { QDate wst( DateHelper::getNthWeek( year, weeknumber, weekstart ) ); if ( weekday != 0 ) { wst = wst.addDays( ( 7 + weekday - weekstart ) % 7 ); appendDateTime( wst, tm, result ); } else { for ( int i = 0; i < 7; ++i ) { appendDateTime( wst, tm, result ); wst = wst.addDays( 1 ); } } done = true; } // weekday is given if ( !done && weekday != 0 ) { QDate dt( year, 1, 1 ); // If type == yearly and month is given, pos is still in month not year! // TODO_Recurrence: Correct handling of n-th BYDAY... int maxloop = 53; bool inMonth = ( type == RecurrenceRule::rMonthly ) || ( type == RecurrenceRule::rYearly && month > 0 ); if ( inMonth && month > 0 ) { dt = QDate( year, month, 1 ); maxloop = 5; } if ( weekdaynr < 0 ) { // From end of period (month, year) => relative to begin of next period if ( inMonth ) { dt = dt.addMonths( 1 ); } else { dt = dt.addYears( 1 ); } } int adj = ( 7 + weekday - dt.dayOfWeek() ) % 7; dt = dt.addDays( adj ); // correct first weekday of the period if ( weekdaynr > 0 ) { dt = dt.addDays( ( weekdaynr - 1 ) * 7 ); appendDateTime( dt, tm, result ); } else if ( weekdaynr < 0 ) { dt = dt.addDays( weekdaynr * 7 ); appendDateTime( dt, tm, result ); } else { // loop through all possible weeks, non-matching will be filtered later for ( int i = 0; i < maxloop; ++i ) { appendDateTime( dt, tm, result ); dt = dt.addDays( 7 ); } } } // weekday != 0 // Only use those times that really match all other constraints, too QList valid; for ( int i = 0, iend = result.count(); i < iend; ++i ) { if ( matches( result[i], type ) ) { valid.append( result[i] ); } } // Don't sort it here, would be unnecessary work. The results from all // constraints will be merged to one big list of the interval. Sort that one! return valid; } void Constraint::appendDateTime( const QDate &date, const QTime &time, QList &list ) const { KDateTime dt( date, time, timespec ); if ( dt.isValid() ) { if ( secondOccurrence ) { dt.setSecondOccurrence( true ); } list.append( dt ); } } bool Constraint::increase( RecurrenceRule::PeriodType type, int freq ) { // convert the first day of the interval to KDateTime intervalDateTime( type ); // Now add the intervals switch ( type ) { case RecurrenceRule::rSecondly: cachedDt = cachedDt.addSecs( freq ); break; case RecurrenceRule::rMinutely: cachedDt = cachedDt.addSecs( 60 * freq ); break; case RecurrenceRule::rHourly: cachedDt = cachedDt.addSecs( 3600 * freq ); break; case RecurrenceRule::rDaily: cachedDt = cachedDt.addDays( freq ); break; case RecurrenceRule::rWeekly: cachedDt = cachedDt.addDays( 7 * freq ); break; case RecurrenceRule::rMonthly: cachedDt = cachedDt.addMonths( freq ); break; case RecurrenceRule::rYearly: cachedDt = cachedDt.addYears( freq ); break; default: break; } // Convert back from KDateTime to the Constraint class readDateTime( cachedDt, type ); useCachedDt = true; // readDateTime() resets this return true; } // Set the constraint's value appropriate to 'type', to the value contained in a date/time. bool Constraint::readDateTime( const KDateTime &dt, RecurrenceRule::PeriodType type ) { switch ( type ) { // Really fall through! Only weekly needs to be treated differently! case RecurrenceRule::rSecondly: second = dt.time().second(); case RecurrenceRule::rMinutely: minute = dt.time().minute(); case RecurrenceRule::rHourly: hour = dt.time().hour(); secondOccurrence = dt.isSecondOccurrence(); case RecurrenceRule::rDaily: day = dt.date().day(); case RecurrenceRule::rMonthly: month = dt.date().month(); case RecurrenceRule::rYearly: year = dt.date().year(); break; case RecurrenceRule::rWeekly: // Determine start day of the current week, calculate the week number from that weeknumber = DateHelper::getWeekNumber( dt.date(), weekstart, &year ); break; default: break; } useCachedDt = false; return true; } //@endcond /************************************************************************** * RecurrenceRule::Private * **************************************************************************/ //@cond PRIVATE class KCal::RecurrenceRule::Private { public: Private( RecurrenceRule *parent ) : mParent( parent ), mPeriod( rNone ), mFrequency( 0 ), mWeekStart( 1 ), mIsReadOnly( false ), mAllDay( false ) {} Private( RecurrenceRule *parent, const Private &p ); Private &operator=( const Private &other ); bool operator==( const Private &other ) const; void clear(); void setDirty(); void buildConstraints(); bool buildCache() const; Constraint getNextValidDateInterval( const KDateTime &preDate, PeriodType type ) const; Constraint getPreviousValidDateInterval( const KDateTime &afterDate, PeriodType type ) const; DateTimeList datesForInterval( const Constraint &interval, PeriodType type ) const; RecurrenceRule *mParent; QString mRRule; // RRULE string PeriodType mPeriod; KDateTime mDateStart; // start of recurrence (but mDateStart is not an occurrence // unless it matches the rule) uint mFrequency; /** how often it recurs: 0 means an explicit end date, positive values give the number of occurrences */ int mDuration; KDateTime mDateEnd; QList mBySeconds; // values: second 0-59 QList mByMinutes; // values: minute 0-59 QList mByHours; // values: hour 0-23 QList mByDays; // n-th weekday of the month or year QList mByMonthDays; // values: day -31 to -1 and 1-31 QList mByYearDays; // values: day -366 to -1 and 1-366 QList mByWeekNumbers; // values: week -53 to -1 and 1-53 QList mByMonths; // values: month 1-12 QList mBySetPos; // values: position -366 to -1 and 1-366 short mWeekStart; // first day of the week (1=Monday, 7=Sunday) Constraint::List mConstraints; QList mObservers; // Cache for duration mutable DateTimeList mCachedDates; mutable KDateTime mCachedDateEnd; mutable KDateTime mCachedLastDate; // when mCachedDateEnd invalid, last date checked mutable bool mCached; bool mIsReadOnly; bool mAllDay; bool mNoByRules; // no BySeconds, ByMinutes, ... rules exist uint mTimedRepetition; // repeats at a regular number of seconds interval, or 0 }; RecurrenceRule::Private::Private( RecurrenceRule *parent, const Private &p ) : mParent( parent ), mRRule( p.mRRule ), mPeriod( p.mPeriod ), mDateStart( p.mDateStart ), mFrequency( p.mFrequency ), mDuration( p.mDuration ), mDateEnd( p.mDateEnd ), mBySeconds( p.mBySeconds ), mByMinutes( p.mByMinutes ), mByHours( p.mByHours ), mByDays( p.mByDays ), mByMonthDays( p.mByMonthDays ), mByYearDays( p.mByYearDays ), mByWeekNumbers( p.mByWeekNumbers ), mByMonths( p.mByMonths ), mBySetPos( p.mBySetPos ), mWeekStart( p.mWeekStart ), mIsReadOnly( p.mIsReadOnly ), mAllDay( p.mAllDay ) { setDirty(); } RecurrenceRule::Private &RecurrenceRule::Private::operator=( const Private &p ) { // check for self assignment if ( &p == this ) { return *this; } mRRule = p.mRRule; mPeriod = p.mPeriod; mDateStart = p.mDateStart; mFrequency = p.mFrequency; mDuration = p.mDuration; mDateEnd = p.mDateEnd; mBySeconds = p.mBySeconds; mByMinutes = p.mByMinutes; mByHours = p.mByHours; mByDays = p.mByDays; mByMonthDays = p.mByMonthDays; mByYearDays = p.mByYearDays; mByWeekNumbers = p.mByWeekNumbers; mByMonths = p.mByMonths; mBySetPos = p.mBySetPos; mWeekStart = p.mWeekStart; mIsReadOnly = p.mIsReadOnly; mAllDay = p.mAllDay; setDirty(); return *this; } bool RecurrenceRule::Private::operator==( const Private &r ) const { return mPeriod == r.mPeriod && mDateStart == r.mDateStart && mDuration == r.mDuration && mDateEnd == r.mDateEnd && mFrequency == r.mFrequency && mIsReadOnly == r.mIsReadOnly && mAllDay == r.mAllDay && mBySeconds == r.mBySeconds && mByMinutes == r.mByMinutes && mByHours == r.mByHours && mByDays == r.mByDays && mByMonthDays == r.mByMonthDays && mByYearDays == r.mByYearDays && mByWeekNumbers == r.mByWeekNumbers && mByMonths == r.mByMonths && mBySetPos == r.mBySetPos && mWeekStart == r.mWeekStart; } void RecurrenceRule::Private::clear() { if ( mIsReadOnly ) { return; } mPeriod = rNone; mBySeconds.clear(); mByMinutes.clear(); mByHours.clear(); mByDays.clear(); mByMonthDays.clear(); mByYearDays.clear(); mByWeekNumbers.clear(); mByMonths.clear(); mBySetPos.clear(); mWeekStart = 1; setDirty(); } void RecurrenceRule::Private::setDirty() { buildConstraints(); mCached = false; mCachedDates.clear(); for ( int i = 0, iend = mObservers.count(); i < iend; ++i ) { if ( mObservers[i] ) { mObservers[i]->recurrenceChanged( mParent ); } } } //@endcond /************************************************************************** * RecurrenceRule * **************************************************************************/ RecurrenceRule::RecurrenceRule() : d( new Private( this ) ) { } RecurrenceRule::RecurrenceRule( const RecurrenceRule &r ) : d( new Private( this, *r.d ) ) { } RecurrenceRule::~RecurrenceRule() { delete d; } bool RecurrenceRule::operator==( const RecurrenceRule &r ) const { return *d == *r.d; } RecurrenceRule &RecurrenceRule::operator=( const RecurrenceRule &r ) { // check for self assignment if ( &r == this ) { return *this; } *d = *r.d; return *this; } void RecurrenceRule::addObserver( RuleObserver *observer ) { if ( !d->mObservers.contains( observer ) ) { d->mObservers.append( observer ); } } void RecurrenceRule::removeObserver( RuleObserver *observer ) { if ( d->mObservers.contains( observer ) ) { d->mObservers.removeAll( observer ); } } void RecurrenceRule::setRecurrenceType( PeriodType period ) { if ( isReadOnly() ) { return; } d->mPeriod = period; d->setDirty(); } KDateTime RecurrenceRule::endDt( bool *result ) const { if ( result ) { *result = false; } if ( d->mPeriod == rNone ) { return KDateTime(); } if ( d->mDuration < 0 ) { return KDateTime(); } if ( d->mDuration == 0 ) { if ( result ) { *result = true; } return d->mDateEnd; } // N occurrences. Check if we have a full cache. If so, return the cached end date. if ( !d->mCached ) { // If not enough occurrences can be found (i.e. inconsistent constraints) if ( !d->buildCache() ) { return KDateTime(); } } if ( result ) { *result = true; } return d->mCachedDateEnd; } void RecurrenceRule::setEndDt( const KDateTime &dateTime ) { if ( isReadOnly() ) { return; } d->mDateEnd = dateTime; d->mDuration = 0; // set to 0 because there is an end date/time d->setDirty(); } void RecurrenceRule::setDuration( int duration ) { if ( isReadOnly() ) { return; } d->mDuration = duration; d->setDirty(); } void RecurrenceRule::setAllDay( bool allDay ) { if ( isReadOnly() ) { return; } d->mAllDay = allDay; d->setDirty(); } void RecurrenceRule::clear() { d->clear(); } void RecurrenceRule::setDirty() { d->setDirty(); } void RecurrenceRule::setStartDt( const KDateTime &start ) { if ( isReadOnly() ) { return; } d->mDateStart = start; d->setDirty(); } void RecurrenceRule::setFrequency( int freq ) { if ( isReadOnly() || freq <= 0 ) { return; } d->mFrequency = freq; d->setDirty(); } void RecurrenceRule::setBySeconds( const QList bySeconds ) { if ( isReadOnly() ) { return; } d->mBySeconds = bySeconds; d->setDirty(); } void RecurrenceRule::setByMinutes( const QList byMinutes ) { if ( isReadOnly() ) { return; } d->mByMinutes = byMinutes; d->setDirty(); } void RecurrenceRule::setByHours( const QList byHours ) { if ( isReadOnly() ) { return; } d->mByHours = byHours; d->setDirty(); } void RecurrenceRule::setByDays( const QList byDays ) { if ( isReadOnly() ) { return; } d->mByDays = byDays; d->setDirty(); } void RecurrenceRule::setByMonthDays( const QList byMonthDays ) { if ( isReadOnly() ) { return; } d->mByMonthDays = byMonthDays; d->setDirty(); } void RecurrenceRule::setByYearDays( const QList byYearDays ) { if ( isReadOnly() ) { return; } d->mByYearDays = byYearDays; d->setDirty(); } void RecurrenceRule::setByWeekNumbers( const QList byWeekNumbers ) { if ( isReadOnly() ) { return; } d->mByWeekNumbers = byWeekNumbers; d->setDirty(); } void RecurrenceRule::setByMonths( const QList byMonths ) { if ( isReadOnly() ) { return; } d->mByMonths = byMonths; d->setDirty(); } void RecurrenceRule::setBySetPos( const QList bySetPos ) { if ( isReadOnly() ) { return; } d->mBySetPos = bySetPos; d->setDirty(); } void RecurrenceRule::setWeekStart( short weekStart ) { if ( isReadOnly() ) { return; } d->mWeekStart = weekStart; d->setDirty(); } void RecurrenceRule::shiftTimes( const KDateTime::Spec &oldSpec, const KDateTime::Spec &newSpec ) { d->mDateStart = d->mDateStart.toTimeSpec( oldSpec ); d->mDateStart.setTimeSpec( newSpec ); if ( d->mDuration == 0 ) { d->mDateEnd = d->mDateEnd.toTimeSpec( oldSpec ); d->mDateEnd.setTimeSpec( newSpec ); } d->setDirty(); } // Taken from recurrence.cpp // int RecurrenceRule::maxIterations() const // { // /* Find the maximum number of iterations which may be needed to reach the // * next actual occurrence of a monthly or yearly recurrence. // * More than one iteration may be needed if, for example, it's the 29th February, // * the 31st day of the month or the 5th Monday, and the month being checked is // * February or a 30-day month. // * The following recurrences may never occur: // * - For rMonthlyDay: if the frequency is a whole number of years. // * - For rMonthlyPos: if the frequency is an even whole number of years. // * - For rYearlyDay, rYearlyMonth: if the frequeny is a multiple of 4 years. // * - For rYearlyPos: if the frequency is an even number of years. // * The maximum number of iterations needed, assuming that it does actually occur, // * was found empirically. // */ // switch (recurs) { // case rMonthlyDay: // return (rFreq % 12) ? 6 : 8; // // case rMonthlyPos: // if (rFreq % 12 == 0) { // // Some of these frequencies may never occur // return (rFreq % 84 == 0) ? 364 // frequency = multiple of 7 years // : (rFreq % 48 == 0) ? 7 // frequency = multiple of 4 years // : (rFreq % 24 == 0) ? 14 : 28; // frequency = multiple of 2 or 1 year // } // // All other frequencies will occur sometime // if (rFreq > 120) // return 364; // frequencies of > 10 years will hit the date limit first // switch (rFreq) { // case 23: return 50; // case 46: return 38; // case 56: return 138; // case 66: return 36; // case 89: return 54; // case 112: return 253; // default: return 25; // most frequencies will need < 25 iterations // } // // case rYearlyMonth: // case rYearlyDay: // return 8; // only 29th Feb or day 366 will need more than one iteration // // case rYearlyPos: // if (rFreq % 7 == 0) // return 364; // frequencies of a multiple of 7 years will hit the date limit first // if (rFreq % 2 == 0) { // // Some of these frequencies may never occur // return (rFreq % 4 == 0) ? 7 : 14; // frequency = even number of years // } // return 28; // } // return 1; // } //@cond PRIVATE void RecurrenceRule::Private::buildConstraints() { mTimedRepetition = 0; mNoByRules = mBySetPos.isEmpty(); mConstraints.clear(); Constraint con( mDateStart.timeSpec() ); if ( mWeekStart > 0 ) { con.setWeekstart( mWeekStart ); } mConstraints.append( con ); int c, cend; int i, iend; Constraint::List tmp; #define intConstraint( list, setElement ) \ if ( !list.isEmpty() ) { \ mNoByRules = false; \ iend = list.count(); \ if ( iend == 1 ) { \ for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \ mConstraints[c].setElement( list[0] ); \ } \ } else { \ for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \ for ( i = 0; i < iend; ++i ) { \ con = mConstraints[c]; \ con.setElement( list[i] ); \ tmp.append( con ); \ } \ } \ mConstraints = tmp; \ tmp.clear(); \ } \ } intConstraint( mBySeconds, setSecond ); intConstraint( mByMinutes, setMinute ); intConstraint( mByHours, setHour ); intConstraint( mByMonthDays, setDay ); intConstraint( mByMonths, setMonth ); intConstraint( mByYearDays, setYearday ); intConstraint( mByWeekNumbers, setWeeknumber ); #undef intConstraint if ( !mByDays.isEmpty() ) { mNoByRules = false; for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { for ( i = 0, iend = mByDays.count(); i < iend; ++i ) { con = mConstraints[c]; con.setWeekday( mByDays[i].day() ); con.setWeekdaynr( mByDays[i].pos() ); tmp.append( con ); } } mConstraints = tmp; tmp.clear(); } #define fixConstraint( setElement, value ) \ { \ for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \ mConstraints[c].setElement( value ); \ } \ } // Now determine missing values from DTSTART. This can speed up things, // because we have more restrictions and save some loops. // TODO: Does RFC 2445 intend to restrict the weekday in all cases of weekly? if ( mPeriod == rWeekly && mByDays.isEmpty() ) { fixConstraint( setWeekday, mDateStart.date().dayOfWeek() ); } // Really fall through in the cases, because all smaller time intervals are // constrained from dtstart switch ( mPeriod ) { case rYearly: if ( mByDays.isEmpty() && mByWeekNumbers.isEmpty() && mByYearDays.isEmpty() && mByMonths.isEmpty() ) { fixConstraint( setMonth, mDateStart.date().month() ); } case rMonthly: if ( mByDays.isEmpty() && mByWeekNumbers.isEmpty() && mByYearDays.isEmpty() && mByMonthDays.isEmpty() ) { fixConstraint( setDay, mDateStart.date().day() ); } case rWeekly: case rDaily: if ( mByHours.isEmpty() ) { fixConstraint( setHour, mDateStart.time().hour() ); } case rHourly: if ( mByMinutes.isEmpty() ) { fixConstraint( setMinute, mDateStart.time().minute() ); } case rMinutely: if ( mBySeconds.isEmpty() ) { fixConstraint( setSecond, mDateStart.time().second() ); } case rSecondly: default: break; } #undef fixConstraint if ( mNoByRules ) { switch ( mPeriod ) { case rHourly: mTimedRepetition = mFrequency * 3600; break; case rMinutely: mTimedRepetition = mFrequency * 60; break; case rSecondly: mTimedRepetition = mFrequency; break; default: break; } } else { for ( c = 0, cend = mConstraints.count(); c < cend; ) { if ( mConstraints[c].isConsistent( mPeriod ) ) { ++c; } else { mConstraints.removeAt( c ); --cend; } } } } // Build and cache a list of all occurrences. // Only call buildCache() if mDuration > 0. bool RecurrenceRule::Private::buildCache() const { // Build the list of all occurrences of this event (we need that to determine // the end date!) Constraint interval( getNextValidDateInterval( mDateStart, mPeriod ) ); QDateTime next; DateTimeList dts = datesForInterval( interval, mPeriod ); // Only use dates after the event has started (start date is only included // if it matches) int i = dts.findLT( mDateStart ); if ( i >= 0 ) { dts.erase( dts.begin(), dts.begin() + i + 1 ); } int loopnr = 0; int dtnr = dts.count(); // some validity checks to avoid infinite loops (i.e. if we have // done this loop already 10000 times, bail out ) while ( loopnr < LOOP_LIMIT && dtnr < mDuration ) { interval.increase( mPeriod, mFrequency ); // The returned date list is already sorted! dts += datesForInterval( interval, mPeriod ); dtnr = dts.count(); ++loopnr; } if ( dts.count() > mDuration ) { // we have picked up more occurrences than necessary, remove them dts.erase( dts.begin() + mDuration, dts.end() ); } mCached = true; mCachedDates = dts; // it = dts.begin(); // while ( it != dts.end() ) { // kDebug() << " -=>" << dumpTime(*it); // ++it; // } if ( int( dts.count() ) == mDuration ) { mCachedDateEnd = dts.last(); return true; } else { // The cached date list is incomplete mCachedDateEnd = KDateTime(); mCachedLastDate = interval.intervalDateTime( mPeriod ); return false; } } //@endcond bool RecurrenceRule::dateMatchesRules( const KDateTime &kdt ) const { KDateTime dt = kdt.toTimeSpec( d->mDateStart.timeSpec() ); for ( int i = 0, iend = d->mConstraints.count(); i < iend; ++i ) { if ( d->mConstraints[i].matches( dt, recurrenceType() ) ) { return true; } } return false; } bool RecurrenceRule::recursOn( const QDate &qd, const KDateTime::Spec &timeSpec ) const { int i, iend; if ( allDay() ) { // It's a date-only rule, so it has no time specification. // Therefore ignore 'timeSpec'. if ( qd < d->mDateStart.date() ) { return false; } // Start date is only included if it really matches QDate endDate; if ( d->mDuration >= 0 ) { endDate = endDt().date(); if ( qd > endDate ) { return false; } } // The date must be in an appropriate interval (getNextValidDateInterval), // Plus it must match at least one of the constraints bool match = false; for ( i = 0, iend = d->mConstraints.count(); i < iend && !match; ++i ) { match = d->mConstraints[i].matches( qd, recurrenceType() ); } if ( !match ) { return false; } KDateTime start( qd, QTime( 0, 0, 0 ), d->mDateStart.timeSpec() ); Constraint interval( d->getNextValidDateInterval( start, recurrenceType() ) ); // Constraint::matches is quite efficient, so first check if it can occur at // all before we calculate all actual dates. if ( !interval.matches( qd, recurrenceType() ) ) { return false; } // We really need to obtain the list of dates in this interval, since // otherwise BYSETPOS will not work (i.e. the date will match the interval, // but BYSETPOS selects only one of these matching dates! KDateTime end = start.addDays(1); do { DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); for ( i = 0, iend = dts.count(); i < iend; ++i ) { if ( dts[i].date() >= qd ) { return dts[i].date() == qd; } } interval.increase( recurrenceType(), frequency() ); } while ( interval.intervalDateTime( recurrenceType() ) < end ); return false; } // It's a date-time rule, so we need to take the time specification into account. KDateTime start( qd, QTime( 0, 0, 0 ), timeSpec ); KDateTime end = start.addDays( 1 ).toTimeSpec( d->mDateStart.timeSpec() ); start = start.toTimeSpec( d->mDateStart.timeSpec() ); if ( end < d->mDateStart ) { return false; } if ( start < d->mDateStart ) { start = d->mDateStart; } // Start date is only included if it really matches if ( d->mDuration >= 0 ) { KDateTime endRecur = endDt(); if ( endRecur.isValid() ) { if ( start > endRecur ) { return false; } if ( end > endRecur ) { end = endRecur; // limit end-of-day time to end of recurrence rule } } } if ( d->mTimedRepetition ) { // It's a simple sub-daily recurrence with no constraints int n = static_cast( ( d->mDateStart.secsTo_long( start ) - 1 ) % d->mTimedRepetition ); return start.addSecs( d->mTimedRepetition - n ) < end; } // Find the start and end dates in the time spec for the rule QDate startDay = start.date(); QDate endDay = end.addSecs( -1 ).date(); int dayCount = startDay.daysTo( endDay ) + 1; // The date must be in an appropriate interval (getNextValidDateInterval), // Plus it must match at least one of the constraints bool match = false; for ( i = 0, iend = d->mConstraints.count(); i < iend && !match; ++i ) { match = d->mConstraints[i].matches( startDay, recurrenceType() ); for ( int day = 1; day < dayCount && !match; ++day ) { match = d->mConstraints[i].matches( startDay.addDays( day ), recurrenceType() ); } } if ( !match ) { return false; } Constraint interval( d->getNextValidDateInterval( start, recurrenceType() ) ); // Constraint::matches is quite efficient, so first check if it can occur at // all before we calculate all actual dates. match = false; Constraint intervalm = interval; do { match = intervalm.matches( startDay, recurrenceType() ); for ( int day = 1; day < dayCount && !match; ++day ) { match = intervalm.matches( startDay.addDays( day ), recurrenceType() ); } if ( match ) { break; } intervalm.increase( recurrenceType(), frequency() ); } while ( intervalm.intervalDateTime( recurrenceType() ) < end ); if ( !match ) { return false; } // We really need to obtain the list of dates in this interval, since // otherwise BYSETPOS will not work (i.e. the date will match the interval, // but BYSETPOS selects only one of these matching dates! do { DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); int i = dts.findGE( start ); if ( i >= 0 ) { return dts[i] <= end; } interval.increase( recurrenceType(), frequency() ); } while ( interval.intervalDateTime( recurrenceType() ) < end ); return false; } bool RecurrenceRule::recursAt( const KDateTime &kdt ) const { // Convert to the time spec used by this recurrence rule KDateTime dt( kdt.toTimeSpec( d->mDateStart.timeSpec() ) ); if ( allDay() ) { return recursOn( dt.date(), dt.timeSpec() ); } if ( dt < d->mDateStart ) { return false; } // Start date is only included if it really matches if ( d->mDuration >= 0 && dt > endDt() ) { return false; } if ( d->mTimedRepetition ) { // It's a simple sub-daily recurrence with no constraints return !( d->mDateStart.secsTo_long( dt ) % d->mTimedRepetition ); } // The date must be in an appropriate interval (getNextValidDateInterval), // Plus it must match at least one of the constraints if ( !dateMatchesRules( dt ) ) { return false; } // if it recurs every interval, speed things up... // if ( d->mFrequency == 1 && d->mBySetPos.isEmpty() && d->mByDays.isEmpty() ) return true; Constraint interval( d->getNextValidDateInterval( dt, recurrenceType() ) ); // TODO_Recurrence: Does this work with BySetPos??? if ( interval.matches( dt, recurrenceType() ) ) { return true; } return false; } TimeList RecurrenceRule::recurTimesOn( const QDate &date, const KDateTime::Spec &timeSpec ) const { TimeList lst; if ( allDay() ) { return lst; } KDateTime start( date, QTime( 0, 0, 0 ), timeSpec ); KDateTime end = start.addDays( 1 ).addSecs( -1 ); DateTimeList dts = timesInInterval( start, end ); // returns between start and end inclusive for ( int i = 0, iend = dts.count(); i < iend; ++i ) { lst += dts[i].toTimeSpec( timeSpec ).time(); } return lst; } /** Returns the number of recurrences up to and including the date/time specified. */ int RecurrenceRule::durationTo( const KDateTime &dt ) const { // Convert to the time spec used by this recurrence rule KDateTime toDate( dt.toTimeSpec( d->mDateStart.timeSpec() ) ); // Easy cases: // either before start, or after all recurrences and we know their number if ( toDate < d->mDateStart ) { return 0; } // Start date is only included if it really matches if ( d->mDuration > 0 && toDate >= endDt() ) { return d->mDuration; } if ( d->mTimedRepetition ) { // It's a simple sub-daily recurrence with no constraints return static_cast( d->mDateStart.secsTo_long( toDate ) / d->mTimedRepetition ); } return timesInInterval( d->mDateStart, toDate ).count(); } int RecurrenceRule::durationTo( const QDate &date ) const { return durationTo( KDateTime( date, QTime( 23, 59, 59 ), d->mDateStart.timeSpec() ) ); } KDateTime RecurrenceRule::getPreviousDate( const KDateTime &afterDate ) const { // Convert to the time spec used by this recurrence rule KDateTime toDate( afterDate.toTimeSpec( d->mDateStart.timeSpec() ) ); // Invalid starting point, or beyond end of recurrence if ( !toDate.isValid() || toDate < d->mDateStart ) { return KDateTime(); } if ( d->mTimedRepetition ) { // It's a simple sub-daily recurrence with no constraints KDateTime prev = toDate; if ( d->mDuration >= 0 && endDt().isValid() && toDate > endDt() ) { prev = endDt().addSecs( 1 ).toTimeSpec( d->mDateStart.timeSpec() ); } int n = static_cast( ( d->mDateStart.secsTo_long( prev ) - 1 ) % d->mTimedRepetition ); if ( n < 0 ) { return KDateTime(); // before recurrence start } prev = prev.addSecs( -n - 1 ); return prev >= d->mDateStart ? prev : KDateTime(); } // If we have a cache (duration given), use that if ( d->mDuration > 0 ) { if ( !d->mCached ) { d->buildCache(); } int i = d->mCachedDates.findLT( toDate ); if ( i >= 0 ) { return d->mCachedDates[i]; } return KDateTime(); } KDateTime prev = toDate; if ( d->mDuration >= 0 && endDt().isValid() && toDate > endDt() ) { prev = endDt().addSecs( 1 ).toTimeSpec( d->mDateStart.timeSpec() ); } Constraint interval( d->getPreviousValidDateInterval( prev, recurrenceType() ) ); DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); int i = dts.findLT( prev ); if ( i >= 0 ) { return ( dts[i] >= d->mDateStart ) ? dts[i] : KDateTime(); } // Previous interval. As soon as we find an occurrence, we're done. while ( interval.intervalDateTime( recurrenceType() ) > d->mDateStart ) { interval.increase( recurrenceType(), -int( frequency() ) ); // The returned date list is sorted DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); // The list is sorted, so take the last one. if ( !dts.isEmpty() ) { prev = dts.last(); if ( prev.isValid() && prev >= d->mDateStart ) { return prev; } else { return KDateTime(); } } } return KDateTime(); } KDateTime RecurrenceRule::getNextDate( const KDateTime &preDate ) const { // Convert to the time spec used by this recurrence rule KDateTime fromDate( preDate.toTimeSpec( d->mDateStart.timeSpec() ) ); // Beyond end of recurrence if ( d->mDuration >= 0 && endDt().isValid() && fromDate >= endDt() ) { return KDateTime(); } // Start date is only included if it really matches if ( fromDate < d->mDateStart ) { fromDate = d->mDateStart.addSecs( -1 ); } if ( d->mTimedRepetition ) { // It's a simple sub-daily recurrence with no constraints int n = static_cast( ( d->mDateStart.secsTo_long( fromDate ) + 1 ) % d->mTimedRepetition ); KDateTime next = fromDate.addSecs( d->mTimedRepetition - n + 1 ); return d->mDuration < 0 || !endDt().isValid() || next <= endDt() ? next : KDateTime(); } if ( d->mDuration > 0 ) { if ( !d->mCached ) { d->buildCache(); } int i = d->mCachedDates.findGT( fromDate ); if ( i >= 0 ) { return d->mCachedDates[i]; } } KDateTime end = endDt(); Constraint interval( d->getNextValidDateInterval( fromDate, recurrenceType() ) ); DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); int i = dts.findGT( fromDate ); if ( i >= 0 ) { return ( d->mDuration < 0 || dts[i] <= end ) ? dts[i] : KDateTime(); } interval.increase( recurrenceType(), frequency() ); if ( d->mDuration >= 0 && interval.intervalDateTime( recurrenceType() ) > end ) { return KDateTime(); } // Increase the interval. The first occurrence that we find is the result (if // if's before the end date). // TODO: some validity checks to avoid infinite loops for contradictory constraints int loop = 0; do { DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); if ( dts.count() > 0 ) { KDateTime ret( dts[0] ); if ( d->mDuration >= 0 && ret > end ) { return KDateTime(); } else { return ret; } } interval.increase( recurrenceType(), frequency() ); } while ( ++loop < LOOP_LIMIT && ( d->mDuration < 0 || interval.intervalDateTime( recurrenceType() ) < end ) ); return KDateTime(); } DateTimeList RecurrenceRule::timesInInterval( const KDateTime &dtStart, const KDateTime &dtEnd ) const { KDateTime start = dtStart.toTimeSpec( d->mDateStart.timeSpec() ); KDateTime end = dtEnd.toTimeSpec( d->mDateStart.timeSpec() ); DateTimeList result; if ( end < d->mDateStart ) { return result; // before start of recurrence } KDateTime enddt = end; if ( d->mDuration >= 0 ) { KDateTime endRecur = endDt(); if ( endRecur.isValid() ) { if ( start >= endRecur ) { return result; // beyond end of recurrence } if ( end > endRecur ) { enddt = endRecur; // limit end time to end of recurrence rule } } } if ( d->mTimedRepetition ) { // It's a simple sub-daily recurrence with no constraints int n = static_cast( ( d->mDateStart.secsTo_long( start ) - 1 ) % d->mTimedRepetition ); KDateTime dt = start.addSecs( d->mTimedRepetition - n ); if ( dt < enddt ) { n = static_cast( ( dt.secsTo_long( enddt ) - 1 ) / d->mTimedRepetition ) + 1; + // limit n by a sane value else we can "explode". + n = qMin( n, LOOP_LIMIT ); for ( int i = 0; i < n; dt = dt.addSecs( d->mTimedRepetition ), ++i ) { result += dt; } } return result; } KDateTime st = start; bool done = false; if ( d->mDuration > 0 ) { if ( !d->mCached ) { d->buildCache(); } if ( d->mCachedDateEnd.isValid() && start >= d->mCachedDateEnd ) { return result; // beyond end of recurrence } int i = d->mCachedDates.findGE( start ); if ( i >= 0 ) { int iend = d->mCachedDates.findGT( enddt, i ); if ( iend < 0 ) { iend = d->mCachedDates.count(); } else { done = true; } while ( i < iend ) { result += d->mCachedDates[i++]; } } if ( d->mCachedDateEnd.isValid() ) { done = true; } else if ( !result.isEmpty() ) { result += KDateTime(); // indicate that the returned list is incomplete done = true; } if ( done ) { return result; } // We don't have any result yet, but we reached the end of the incomplete cache st = d->mCachedLastDate.addSecs( 1 ); } Constraint interval( d->getNextValidDateInterval( st, recurrenceType() ) ); int loop = 0; do { DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); int i = 0; int iend = dts.count(); if ( loop == 0 ) { i = dts.findGE( st ); if ( i < 0 ) { i = iend; } } int j = dts.findGT( enddt, i ); if ( j >= 0 ) { iend = j; loop = LOOP_LIMIT; } while ( i < iend ) { result += dts[i++]; } // Increase the interval. interval.increase( recurrenceType(), frequency() ); } while ( ++loop < LOOP_LIMIT && interval.intervalDateTime( recurrenceType() ) < end ); return result; } //@cond PRIVATE // Find the date/time of the occurrence at or before a date/time, // for a given period type. // Return a constraint whose value appropriate to 'type', is set to // the value contained in the date/time. Constraint RecurrenceRule::Private::getPreviousValidDateInterval( const KDateTime &dt, PeriodType type ) const { long periods = 0; KDateTime start = mDateStart; KDateTime nextValid( start ); int modifier = 1; KDateTime toDate( dt.toTimeSpec( start.timeSpec() ) ); // for super-daily recurrences, don't care about the time part // Find the #intervals since the dtstart and round to the next multiple of // the frequency switch ( type ) { // Really fall through for sub-daily, since the calculations only differ // by the factor 60 and 60*60! Same for weekly and daily (factor 7) case rHourly: modifier *= 60; case rMinutely: modifier *= 60; case rSecondly: periods = static_cast( start.secsTo_long( toDate ) / modifier ); // round it down to the next lower multiple of frequency: if ( mFrequency > 0 ) { periods = ( periods / mFrequency ) * mFrequency; } nextValid = start.addSecs( modifier * periods ); break; case rWeekly: toDate = toDate.addDays( -( 7 + toDate.date().dayOfWeek() - mWeekStart ) % 7 ); start = start.addDays( -( 7 + start.date().dayOfWeek() - mWeekStart ) % 7 ); modifier *= 7; case rDaily: periods = start.daysTo( toDate ) / modifier; // round it down to the next lower multiple of frequency: if ( mFrequency > 0 ) { periods = ( periods / mFrequency ) * mFrequency; } nextValid = start.addDays( modifier * periods ); break; case rMonthly: { periods = 12 * ( toDate.date().year() - start.date().year() ) + ( toDate.date().month() - start.date().month() ); // round it down to the next lower multiple of frequency: if ( mFrequency > 0 ) { periods = ( periods / mFrequency ) * mFrequency; } // set the day to the first day of the month, so we don't have problems // with non-existent days like Feb 30 or April 31 start.setDate( QDate( start.date().year(), start.date().month(), 1 ) ); nextValid.setDate( start.date().addMonths( periods ) ); break; } case rYearly: periods = ( toDate.date().year() - start.date().year() ); // round it down to the next lower multiple of frequency: if ( mFrequency > 0 ) { periods = ( periods / mFrequency ) * mFrequency; } nextValid.setDate( start.date().addYears( periods ) ); break; default: break; } return Constraint( nextValid, type, mWeekStart ); } // Find the date/time of the next occurrence at or after a date/time, // for a given period type. // Return a constraint whose value appropriate to 'type', is set to the // value contained in the date/time. Constraint RecurrenceRule::Private::getNextValidDateInterval( const KDateTime &dt, PeriodType type ) const { // TODO: Simplify this! long periods = 0; KDateTime start = mDateStart; KDateTime nextValid( start ); int modifier = 1; KDateTime toDate( dt.toTimeSpec( start.timeSpec() ) ); // for super-daily recurrences, don't care about the time part // Find the #intervals since the dtstart and round to the next multiple of // the frequency switch ( type ) { // Really fall through for sub-daily, since the calculations only differ // by the factor 60 and 60*60! Same for weekly and daily (factor 7) case rHourly: modifier *= 60; case rMinutely: modifier *= 60; case rSecondly: periods = static_cast( start.secsTo_long( toDate ) / modifier ); periods = qMax( 0L, periods ); if ( periods > 0 && mFrequency > 0 ) { periods += ( mFrequency - 1 - ( ( periods - 1 ) % mFrequency ) ); } nextValid = start.addSecs( modifier * periods ); break; case rWeekly: // correct both start date and current date to start of week toDate = toDate.addDays( -( 7 + toDate.date().dayOfWeek() - mWeekStart ) % 7 ); start = start.addDays( -( 7 + start.date().dayOfWeek() - mWeekStart ) % 7 ); modifier *= 7; case rDaily: periods = start.daysTo( toDate ) / modifier; periods = qMax( 0L, periods ); if ( periods > 0 && mFrequency > 0 ) { periods += ( mFrequency - 1 - ( ( periods - 1 ) % mFrequency ) ); } nextValid = start.addDays( modifier * periods ); break; case rMonthly: { periods = 12 * ( toDate.date().year() - start.date().year() ) + ( toDate.date().month() - start.date().month() ); periods = qMax( 0L, periods ); if ( periods > 0 && mFrequency > 0 ) { periods += ( mFrequency - 1 - ( ( periods - 1 ) % mFrequency ) ); } // set the day to the first day of the month, so we don't have problems // with non-existent days like Feb 30 or April 31 start.setDate( QDate( start.date().year(), start.date().month(), 1 ) ); nextValid.setDate( start.date().addMonths( periods ) ); break; } case rYearly: periods = ( toDate.date().year() - start.date().year() ); periods = qMax( 0L, periods ); if ( periods > 0 && mFrequency > 0 ) { periods += ( mFrequency - 1 - ( ( periods - 1 ) % mFrequency ) ); } nextValid.setDate( start.date().addYears( periods ) ); break; default: break; } return Constraint( nextValid, type, mWeekStart ); } DateTimeList RecurrenceRule::Private::datesForInterval( const Constraint &interval, PeriodType type ) const { /* -) Loop through constraints, -) merge interval with each constraint -) if merged constraint is not consistent => ignore that constraint -) if complete => add that one date to the date list -) Loop through all missing fields => For each add the resulting */ DateTimeList lst; for ( int i = 0, iend = mConstraints.count(); i < iend; ++i ) { Constraint merged( interval ); if ( merged.merge( mConstraints[i] ) ) { // If the information is incomplete, we can't use this constraint if ( merged.year > 0 && merged.hour >= 0 && merged.minute >= 0 && merged.second >= 0 ) { // We have a valid constraint, so get all datetimes that match it andd // append it to all date/times of this interval QList lstnew = merged.dateTimes( type ); lst += lstnew; } } } // Sort it so we can apply the BySetPos. Also some logic relies on this being sorted lst.sortUnique(); /*if ( lst.isEmpty() ) { kDebug() << " No Dates in Interval"; } else { kDebug() << " Dates:"; for ( int i = 0, iend = lst.count(); i < iend; ++i ) { kDebug()<< " -)" << dumpTime(lst[i]); } kDebug() << " ---------------------"; }*/ if ( !mBySetPos.isEmpty() ) { DateTimeList tmplst = lst; lst.clear(); for ( int i = 0, iend = mBySetPos.count(); i < iend; ++i ) { int pos = mBySetPos[i]; if ( pos > 0 ) { --pos; } if ( pos < 0 ) { pos += tmplst.count(); } if ( pos >= 0 && pos < tmplst.count() ) { lst.append( tmplst[pos] ); } } lst.sortUnique(); } return lst; } //@endcond void RecurrenceRule::dump() const { #ifndef NDEBUG kDebug(); if ( !d->mRRule.isEmpty() ) { kDebug() << " RRULE=" << d->mRRule; } kDebug() << " Read-Only:" << isReadOnly(); kDebug() << " Period type:" << recurrenceType() << ", frequency:" << frequency(); kDebug() << " #occurrences:" << duration(); kDebug() << " start date:" << dumpTime( startDt() ) << ", end date:" << dumpTime( endDt() ); #define dumpByIntList(list,label) \ if ( !list.isEmpty() ) {\ QStringList lst;\ for ( int i = 0, iend = list.count(); i < iend; ++i ) {\ lst.append( QString::number( list[i] ) );\ }\ kDebug() << " " << label << lst.join( ", " );\ } dumpByIntList( d->mBySeconds, "BySeconds: " ); dumpByIntList( d->mByMinutes, "ByMinutes: " ); dumpByIntList( d->mByHours, "ByHours: " ); if ( !d->mByDays.isEmpty() ) { QStringList lst; for ( int i = 0, iend = d->mByDays.count(); i < iend; ++i ) {\ lst.append( ( d->mByDays[i].pos() ? QString::number( d->mByDays[i].pos() ) : "" ) + DateHelper::dayName( d->mByDays[i].day() ) ); } kDebug() << " ByDays: " << lst.join( ", " ); } dumpByIntList( d->mByMonthDays, "ByMonthDays:" ); dumpByIntList( d->mByYearDays, "ByYearDays: " ); dumpByIntList( d->mByWeekNumbers, "ByWeekNr: " ); dumpByIntList( d->mByMonths, "ByMonths: " ); dumpByIntList( d->mBySetPos, "BySetPos: " ); #undef dumpByIntList kDebug() << " Week start:" << DateHelper::dayName( d->mWeekStart ); kDebug() << " Constraints:"; // dump constraints for ( int i = 0, iend = d->mConstraints.count(); i < iend; ++i ) { d->mConstraints[i].dump(); } #endif } //@cond PRIVATE void Constraint::dump() const { kDebug() << " ~> Y=" << year << ", M=" << month << ", D=" << day << ", H=" << hour << ", m=" << minute << ", S=" << second << ", wd=" << weekday << ",#wd=" << weekdaynr << ", #w=" << weeknumber << ", yd=" << yearday; } //@endcond QString dumpTime( const KDateTime &dt ) { #ifndef NDEBUG if ( !dt.isValid() ) { return QString(); } QString result; if ( dt.isDateOnly() ) { result = dt.toString( "%a %Y-%m-%d %:Z" ); } else { result = dt.toString( "%a %Y-%m-%d %H:%M:%S %:Z" ); if ( dt.isSecondOccurrence() ) { result += QLatin1String( " (2nd)" ); } } if ( dt.timeSpec() == KDateTime::Spec::ClockTime() ) { result += QLatin1String( "Clock" ); } return result; #else Q_UNUSED( dt ); return QString(); #endif } KDateTime RecurrenceRule::startDt() const { return d->mDateStart; } RecurrenceRule::PeriodType RecurrenceRule::recurrenceType() const { return d->mPeriod; } uint RecurrenceRule::frequency() const { return d->mFrequency; } int RecurrenceRule::duration() const { return d->mDuration; } QString RecurrenceRule::rrule() const { return d->mRRule; } void RecurrenceRule::setRRule( const QString &rrule ) { d->mRRule = rrule; } bool RecurrenceRule::isReadOnly() const { return d->mIsReadOnly; } void RecurrenceRule::setReadOnly( bool readOnly ) { d->mIsReadOnly = readOnly; } bool RecurrenceRule::recurs() const { return d->mPeriod != rNone; } bool RecurrenceRule::allDay() const { return d->mAllDay; } const QList &RecurrenceRule::bySeconds() const { return d->mBySeconds; } const QList &RecurrenceRule::byMinutes() const { return d->mByMinutes; } const QList &RecurrenceRule::byHours() const { return d->mByHours; } const QList &RecurrenceRule::byDays() const { return d->mByDays; } const QList &RecurrenceRule::byMonthDays() const { return d->mByMonthDays; } const QList &RecurrenceRule::byYearDays() const { return d->mByYearDays; } const QList &RecurrenceRule::byWeekNumbers() const { return d->mByWeekNumbers; } const QList &RecurrenceRule::byMonths() const { return d->mByMonths; } const QList &RecurrenceRule::bySetPos() const { return d->mBySetPos; } short RecurrenceRule::weekStart() const { return d->mWeekStart; } diff --git a/kcal/tests/CMakeLists.txt b/kcal/tests/CMakeLists.txt index 55d9623f6..5a8a0bab1 100644 --- a/kcal/tests/CMakeLists.txt +++ b/kcal/tests/CMakeLists.txt @@ -1,122 +1,123 @@ set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) include_directories( ${CMAKE_SOURCE_DIR}/kcal ${LIBICAL_INCLUDE_DIRS} ) MACRO(KCAL_UNIT_TESTS) FOREACH(_testname ${ARGN}) kde4_add_unit_test(${_testname} NOGUI ${_testname}.cpp) target_link_libraries(${_testname} ${KDE4_KDECORE_LIBS} kcal ${QT_QTTEST_LIBRARY} ${QT_QTGUI_LIBRARY} ${LIBICAL_LIBRARIES}) ENDFOREACH(_testname) ENDMACRO(KCAL_UNIT_TESTS) MACRO(KCAL_EXECUTABLE_TESTS) FOREACH(_testname ${ARGN}) kde4_add_executable(${_testname} NOGUI TEST ${_testname}.cpp) target_link_libraries(${_testname} ${KDE4_KDECORE_LIBS} ${KDE4_KDEUI_LIBS} kcal ${QT_QTTEST_LIBRARY} kresources) ENDFOREACH(_testname) ENDMACRO(KCAL_EXECUTABLE_TESTS) KCAL_UNIT_TESTS( testalarm testassignmentvisitor testattachment testattendee testcalendarlocal testcalendarnull # testcalendarresources disable for now because it cause akonadi to start testcalfilter testcomparisonvisitor + testcustomproperties testduration testerrorformat testevent testfilestorage testfreebusy testincidencerelation testjournal testkresult testperiod testperson testsortablelist testtodo ) # this test cannot work with msvc because libical should not be altered # and therefore we can't add KCAL_EXPORT there # it should work fine with mingw because of the auto-import feature if(NOT MSVC) KCAL_UNIT_TESTS(testicaltimezones) endif(NOT MSVC) KCAL_EXECUTABLE_TESTS( convertqtopia incidencestest loadcalendar fbrecurring readandwrite testfb testrecurprevious testrecurrence testrecurrencetype testrecurson testresource testtostring testvcalexport ) ########### next target ############### set(testfields_SRCS testfields.cpp) set(srcfile "${CMAKE_SOURCE_DIR}/kcal/tests/data/test_pilot.ics") add_definitions( -D_TESTINPUT="\\"${srcfile}\\"" ) kde4_add_executable(testfields TEST ${testfields_SRCS}) target_link_libraries(testfields ${KDE4_KDECORE_LIBS} kcal ) ########### Tests ####################### FILE( GLOB_RECURSE testFiles data/RecurrenceRule/*.ics ) FILE( GLOB_RECURSE compatFiles data/Compat/*.ics ) FILE( GLOB_RECURSE vCalFilesAsIcal data/vCalendar/*.ics ) FILE( GLOB_RECURSE vCalFiles data/vCalendar/*.vcs ) FOREACH( file ${testFiles} ) GET_FILENAME_COMPONENT( fn ${file} NAME) ADD_TEST( RecurNext-${fn} ${CMAKE_CURRENT_SOURCE_DIR}/runsingletestcase.pl ${EXECUTABLE_OUTPUT_PATH}/testrecurrence.shell "next" ${file} ) ENDFOREACH(file) FOREACH( file ${testFiles} ) GET_FILENAME_COMPONENT( fn ${file} NAME) ADD_TEST( RecurPrev-${fn} ${CMAKE_CURRENT_SOURCE_DIR}/runsingletestcase.pl ${EXECUTABLE_OUTPUT_PATH}/testrecurprevious.shell "prev" ${file} ) ENDFOREACH(file) FOREACH( file ${testFiles} ) GET_FILENAME_COMPONENT( fn ${file} NAME) ADD_TEST( RecursOn-${fn} ${CMAKE_CURRENT_SOURCE_DIR}/runsingletestcase.pl ${EXECUTABLE_OUTPUT_PATH}/testrecurson.shell "recurson" ${file} ) ENDFOREACH(file) FOREACH( file ${compatFiles} ) GET_FILENAME_COMPONENT( fn ${file} NAME) ADD_TEST( Compat-${fn} ${CMAKE_CURRENT_SOURCE_DIR}/runsingletestcase.pl ${EXECUTABLE_OUTPUT_PATH}/readandwrite.shell "ical" ${file} ) ENDFOREACH(file) FOREACH( file ${vCalFilesAsIcal} ) GET_FILENAME_COMPONENT( fn ${file} NAME) ADD_TEST( VCalOut-${fn} ${CMAKE_CURRENT_SOURCE_DIR}/runsingletestcase.pl ${EXECUTABLE_OUTPUT_PATH}/testvcalexport.shell "vcal" ${file} ) ENDFOREACH(file) FOREACH( file ${vCalFiles} ) GET_FILENAME_COMPONENT( fn ${file} NAME) ADD_TEST( VCalIn-${fn} ${CMAKE_CURRENT_SOURCE_DIR}/runsingletestcase.pl ${EXECUTABLE_OUTPUT_PATH}/readandwrite.shell "ical" ${file} ) ENDFOREACH(file) # ADD_TEST( TestRecurrence runtestcase.pl testrecurrence "next" ${CMAKE_CURRENT_SOURCE_DIR} data/RecurrenceRule/ "*.ics" ) # ADD_TEST( TestRecPrevious runtestcase.pl testrecurprevious "prev" ${CMAKE_CURRENT_SOURCE_DIR} data/RecurrenceRule/ "*.ics" ) # ADD_TEST( TestRecursOn runtestcase.pl testrecurson "recurson" ${CMAKE_CURRENT_SOURCE_DIR} data/RecurrenceRule/ "*.ics" ) # ADD_TEST( TestCompat runtestcase.pl readandwrite "ical" ${CMAKE_CURRENT_SOURCE_DIR} data/Compat/ "*.ics" ) # ADD_TEST( TestVCal runtestcase.pl testvcalexport "vcal" ${CMAKE_CURRENT_SOURCE_DIR} data/vCalendar/ "*.ics" ) # ADD_TEST( TestFromVCal runtestcase.pl readandwrite "ical" ${CMAKE_CURRENT_SOURCE_DIR} data/vCalendar/ "*.vcs" ) diff --git a/kcal/tests/data/Compat/KOrganizer_3.1.ics.ical.ref b/kcal/tests/data/Compat/KOrganizer_3.1.ics.ical.ref index e8f2056b3..cadc46ea4 100644 --- a/kcal/tests/data/Compat/KOrganizer_3.1.ics.ical.ref +++ b/kcal/tests/data/Compat/KOrganizer_3.1.ics.ical.ref @@ -1,255 +1,254 @@ BEGIN:VCALENDAR PRODID:-//K Desktop Environment//NONSGML libkcal 3.5//EN VERSION:2.0 BEGIN:VEVENT DTSTAMP:20040122T092700Z ORGANIZER:MAILTO:nobody@nowhere X-PILOTID:0 X-PILOTSTAT:1 CREATED:20031213T212250Z UID:KOrganizer-1744115041.851 SEQUENCE:2 LAST-MODIFIED:20031213T212259Z DESCRIPTION:Two day all-day event.\n SUMMARY:All Day 2 PRIORITY:5 DTSTART;VALUE=DATE:20031209 DTEND;VALUE=DATE:20031211 TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT DTSTAMP:20040122T092700Z ORGANIZER:MAILTO:nobody@nowhere X-PILOTID:0 X-PILOTSTAT:1 CREATED:20031213T212311Z UID:KOrganizer-1927781860.81 SEQUENCE:2 LAST-MODIFIED:20031213T212315Z DESCRIPTION:Three day all-day event.\n SUMMARY:All day 3 PRIORITY:5 DTSTART;VALUE=DATE:20031210 DTEND;VALUE=DATE:20031213 TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT DTSTAMP:20040122T092700Z ORGANIZER:MAILTO:nobody@nowhere ATTENDEE;CN="Another Name";RSVP=TRUE;PARTSTAT=ACCEPTED; ROLE=NON-PARTICIPANT:mailto:two@yyy.yy ATTENDEE;CN="One Name";RSVP=FALSE;PARTSTAT=NEEDS-ACTION; ROLE=REQ-PARTICIPANT:mailto:one@xxx.xx X-PILOTID:0 X-PILOTSTAT:1 CREATED:20031213T212528Z UID:KOrganizer-2056335629.565 SEQUENCE:1 LAST-MODIFIED:20031213T212636Z DESCRIPTION:Reminder 13 minutes\nShow time as free\nCategory Special Occasion\nAccess Private\nTwo attendees.\nAlert.wav sound alarm\n SUMMARY:Full Information Event LOCATION:This is a location CLASS:PRIVATE PRIORITY:5 CATEGORIES:Special Occasion DTSTART:20031211T133000Z DTEND:20031211T154500Z TRANSP:OPAQUE BEGIN:VALARM ATTACH:/usr/share/apps/korganizer/sounds/alert.wav ACTION:AUDIO TRIGGER;VALUE=DURATION:-PT13M END:VALARM END:VEVENT BEGIN:VEVENT DTSTAMP:20040122T092700Z ORGANIZER:MAILTO:nobody@nowhere X-PILOTID:0 X-PILOTSTAT:1 CREATED:20031213T213054Z UID:KOrganizer-1149443407.784 SEQUENCE:1 LAST-MODIFIED:20031213T213109Z DESCRIPTION:12.12. 17:15 to 13.12. 19:15\n1 hour alarm SUMMARY:Multi-Day Event with Start/End Time PRIORITY:5 DTSTART:20031212T171500Z DTEND:20031213T191500Z TRANSP:OPAQUE BEGIN:VALARM DESCRIPTION: ACTION:DISPLAY TRIGGER;VALUE=DURATION:-PT1H END:VALARM END:VEVENT BEGIN:VEVENT DTSTAMP:20040122T092700Z ORGANIZER:MAILTO:nobody@nowhere X-PILOTID:0 X-PILOTSTAT:1 CREATED:20031213T212219Z UID:KOrganizer-182017314.321 SEQUENCE:2 LAST-MODIFIED:20031213T212229Z DESCRIPTION:One day all-day event.\nCategories Birthday\, Personal\n SUMMARY:All Day PRIORITY:5 -CATEGORIES:Birthday -CATEGORIES:Personal +CATEGORIES:Birthday\,Personal DTSTART;VALUE=DATE:20031212 DTEND;VALUE=DATE:20031213 TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT DTSTAMP:20040122T092700Z ORGANIZER:MAILTO:nobody@nowhere X-PILOTID:0 X-PILOTSTAT:1 CREATED:20031213T212707Z UID:KOrganizer-1025455571.504 SEQUENCE:1 LAST-MODIFIED:20031213T212727Z DESCRIPTION:Recurring weekly three times on wednesday SUMMARY:Recurring weekly PRIORITY:5 RRULE:FREQ=WEEKLY;COUNT=3;BYDAY=WE DTSTART:20031210T073000Z DTEND:20031210T083000Z TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT DTSTAMP:20040122T092700Z ORGANIZER:MAILTO:nobody@nowhere X-PILOTID:0 X-PILOTSTAT:1 CREATED:20031213T213318Z UID:KOrganizer-951116794.484 LAST-MODIFIED:20031213T213318Z DESCRIPTION:Recurring until 10.12.\nException 9.12.\n SUMMARY:Recurring with exception PRIORITY:5 RRULE:FREQ=DAILY;UNTIL=20031210 EXDATE;VALUE=DATE:20031209 DTSTART:20031208T163000Z DTEND:20031208T184500Z TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT DTSTAMP:20040122T092700Z ORGANIZER:MAILTO:nobody@nowhere X-PILOTID:0 X-PILOTSTAT:1 CREATED:20031213T162226Z UID:KOrganizer-406380218.717 SEQUENCE:1 LAST-MODIFIED:20031213T212040Z DESCRIPTION:10:00 - 13:30 (UTC)\n SUMMARY:An Event PRIORITY:5 DTSTART:20031213T100000Z DTEND:20031213T133000Z TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT DTSTAMP:20040122T092700Z ORGANIZER:MAILTO:nobody@nowhere X-PILOTID:0 X-PILOTSTAT:1 CREATED:20031213T212117Z UID:KOrganizer-856020879.491 LAST-MODIFIED:20031213T212117Z DESCRIPTION:9:30 - 11:15 (UTC)\nRecurring twice SUMMARY:Another Event PRIORITY:5 RRULE:FREQ=DAILY;COUNT=2 DTSTART:20031209T093000Z DTEND:20031209T111500Z TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT DTSTAMP:20040122T092700Z ORGANIZER:MAILTO:nobody@nowhere X-PILOTID:0 X-PILOTSTAT:1 CREATED:20031213T212933Z UID:KOrganizer-475330509.201 LAST-MODIFIED:20031213T212933Z DESCRIPTION:See Week 50 2003 for more events.\nRecur daily forever.\n SUMMARY:NAVIGATION EVENT PRIORITY:5 RRULE:FREQ=DAILY DTSTART:20031208T120000Z DTEND:20031208T130000Z TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT DTSTAMP:20040122T092700Z ORGANIZER:MAILTO:nobody@nowhere X-PILOTID:0 X-PILOTSTAT:1 CREATED:20031213T212804Z UID:KOrganizer-465691683.1018 LAST-MODIFIED:20031213T212804Z SUMMARY:Recurring monthly until 1.1.2005 PRIORITY:5 RRULE:FREQ=MONTHLY;UNTIL=20050101;BYMONTHDAY=11 DTSTART:20031211T084500Z DTEND:20031211T103000Z TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT DTSTAMP:20040122T092700Z ORGANIZER:MAILTO:nobody@nowhere X-PILOTID:0 X-PILOTSTAT:1 CREATED:20031213T213154Z UID:KOrganizer-1952016504.437 SEQUENCE:1 LAST-MODIFIED:20031213T213204Z SUMMARY:Birthday PRIORITY:5 CATEGORIES:Birthday RRULE:FREQ=YEARLY;BYMONTH=12 DTSTART;VALUE=DATE:20031208 DTEND;VALUE=DATE:20031209 TRANSP:OPAQUE BEGIN:VALARM DESCRIPTION: ACTION:DISPLAY TRIGGER;VALUE=DURATION:-P1D END:VALARM END:VEVENT BEGIN:VEVENT DTSTAMP:20040122T092700Z ORGANIZER:MAILTO:nobody@nowhere X-PILOTID:0 X-PILOTSTAT:1 CREATED:20031213T213357Z UID:KOrganizer-659286401.509 SEQUENCE:1 LAST-MODIFIED:20031213T213426Z SUMMARY:Recurring three times two days interval PRIORITY:5 RRULE:FREQ=DAILY;COUNT=3;INTERVAL=2 DTSTART:20031208T194500Z DTEND:20031208T213000Z TRANSP:OPAQUE END:VEVENT END:VCALENDAR diff --git a/kcal/tests/data/Compat/KOrganizer_3.2.ics.ical.ref b/kcal/tests/data/Compat/KOrganizer_3.2.ics.ical.ref index 2c00cf19b..de2789d33 100644 --- a/kcal/tests/data/Compat/KOrganizer_3.2.ics.ical.ref +++ b/kcal/tests/data/Compat/KOrganizer_3.2.ics.ical.ref @@ -1,82 +1,81 @@ BEGIN:VCALENDAR PRODID:-//K Desktop Environment//NONSGML libkcal 3.5//EN VERSION:2.0 BEGIN:VEVENT DTSTAMP:20040122T092701Z ORGANIZER:MAILTO:nobody@nowhere CREATED:20031213T204645Z UID:KOrganizer-367556625.513 SEQUENCE:2 LAST-MODIFIED:20031213T204657Z SUMMARY:Two-day all day event recurring twice every second month PRIORITY:5 RRULE:FREQ=MONTHLY;COUNT=2;INTERVAL=2;BYMONTHDAY=10 DTSTART;VALUE=DATE:20031210 DTEND;VALUE=DATE:20031212 TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT DTSTAMP:20040122T092701Z ORGANIZER:MAILTO:nobody@nowhere CREATED:20031213T204326Z UID:KOrganizer-178854360.121 LAST-MODIFIED:20031213T204326Z DESCRIPTION:Reminder 2 hours\nShow time as free\nCategories meeting\, phone call\nUmlauts: äöüÄÖÜß\nAccess: confidential\nRecurrence: twice weekly on monday.\n SUMMARY:Full Info LOCATION:location CLASS:CONFIDENTIAL PRIORITY:5 -CATEGORIES:Meeting -CATEGORIES:Phone Call +CATEGORIES:Meeting\,Phone Call RRULE:FREQ=WEEKLY;COUNT=2;BYDAY=MO EXDATE:20031209T091500Z DTSTART:20031209T091500Z DTEND:20031209T113000Z TRANSP:TRANSPARENT BEGIN:VALARM DESCRIPTION: ACTION:DISPLAY TRIGGER;VALUE=DURATION:-PT2H END:VALARM END:VEVENT BEGIN:VEVENT DTSTAMP:20040122T092701Z ORGANIZER:MAILTO:nobody@nowhere ATTENDEE;CN="Hans Wurst";RSVP=TRUE;PARTSTAT=TENTATIVE;ROLE=CHAIR:mailto: hw@abc.de CREATED:20031213T204544Z UID:KOrganizer-581715779.572 LAST-MODIFIED:20031213T204544Z DESCRIPTION:8.12. - 11.12 13:30 - 10:30\nreminder 40 hours\nOne Attendee SUMMARY:Multi-Day Event PRIORITY:5 DTSTART:20031208T123000Z DTEND:20031211T093000Z TRANSP:OPAQUE BEGIN:VALARM DESCRIPTION: ACTION:DISPLAY TRIGGER;VALUE=DURATION:-P1DT16H END:VALARM END:VEVENT BEGIN:VEVENT DTSTAMP:20040122T092701Z ORGANIZER:MAILTO:nobody@nowhere CREATED:20031213T204152Z UID:KOrganizer-101218390.515 LAST-MODIFIED:20031213T204152Z SUMMARY:Holladiho PRIORITY:5 DTSTART:20031213T071500Z DTEND:20031213T104500Z TRANSP:OPAQUE END:VEVENT END:VCALENDAR diff --git a/kcal/tests/testcustomproperties.cpp b/kcal/tests/testcustomproperties.cpp new file mode 100644 index 000000000..e06007f47 --- /dev/null +++ b/kcal/tests/testcustomproperties.cpp @@ -0,0 +1,152 @@ +/* + This file is part of the kcal library. + Copyright (c) 2009 Allen Winter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +#include "testcustomproperties.h" +#include "testcustomproperties.moc" +#include + +QTEST_KDEMAIN( CustomPropertiesTest, NoGUI ) + +#include "kcal/customproperties.h" +using namespace KCal; + +void CustomPropertiesTest::testValidity() +{ + CustomProperties cp; + + QByteArray app( "KORG" ); + QByteArray key( "TEXT" ); + + cp.setCustomProperty( app, key, "rich" ); + QCOMPARE( cp.customProperty( app, key ), QString( "rich" ) ); + + cp.removeCustomProperty( app, key ); + cp.setCustomProperty( app, key, "foo" ); + cp.setCustomProperty( app, key, "rich" ); + QCOMPARE( cp.customProperty( app, key ), QString( "rich" ) ); + + key = "X-TEXT"; + cp.setNonKDECustomProperty( key, "rich" ); + QCOMPARE( cp.nonKDECustomProperty( key ), QString( "rich" ) ); + + cp.removeNonKDECustomProperty( key ); + cp.setNonKDECustomProperty( key, "foo" ); + cp.setNonKDECustomProperty( key, "rich" ); + QCOMPARE( cp.nonKDECustomProperty( key ), QString( "rich" ) ); +} + +void CustomPropertiesTest::testCompare() +{ + CustomProperties cp1, cp2; + + QByteArray app( "KORG" ); + QByteArray key( "TEXT" ); + + cp1.setCustomProperty( app, key, "rich" ); + cp2 = cp1; + QVERIFY( cp1 == cp2 ); + + CustomProperties cp3; + cp3.setCustomProperty( app, key, cp1.customProperty( app, key ) ); + QVERIFY( cp1 == cp3 ); + + QVERIFY( cp1.customProperty( app, key ) == QString( "rich" ) ); + QVERIFY( cp1.customProperty( app, "foo" ) == QString() ); + QVERIFY( cp1.customProperty( app, QByteArray() ) == QString() ); + + CustomProperties cp4; + QVERIFY( cp4.customProperty( app, key ) == QString() ); + QVERIFY( cp4.customProperty( app, "foo" ) == QString() ); + QVERIFY( cp4.customProperty( app, QByteArray() ) == QString() ); + + key = "X-TEXT"; + cp1.setNonKDECustomProperty( key, "rich" ); + cp2 = cp1; + QVERIFY( cp1 == cp2 ); + + cp3.setNonKDECustomProperty( key, cp1.nonKDECustomProperty( key ) ); + QVERIFY( cp1 == cp3 ); + + QVERIFY( cp1.nonKDECustomProperty( key ) == QString( "rich" ) ); + QVERIFY( cp1.nonKDECustomProperty( "foo" ) == QString() ); + QVERIFY( cp1.nonKDECustomProperty( QByteArray() ) == QString() ); + + CustomProperties cp5; + QVERIFY( cp5.nonKDECustomProperty( key ) == QString() ); + QVERIFY( cp5.nonKDECustomProperty( "foo" ) == QString() ); + QVERIFY( cp5.nonKDECustomProperty( QByteArray() ) == QString() ); +} + +void CustomPropertiesTest::testMapValidity() +{ + QMap cpmap; + cpmap.insert( "X-key1", QString( "val1" ) ); + cpmap.insert( "X-key2", QString( "val2" ) ); + cpmap.insert( "X-key3", QString( "val3" ) ); + cpmap.insert( "X-key4", QString( "val4" ) ); + cpmap.insert( "X-key5", QString( "val5" ) ); + + CustomProperties cp; + cp.setCustomProperties( cpmap ); + + QVERIFY( cp.customProperties().value( "X-key3" ) == QString( "val3" ) ); +} + +void CustomPropertiesTest::testMapCompare() +{ + QMap cpmap; + cpmap.insert( "X-key1", QString( "val1" ) ); + cpmap.insert( "X-key2", QString( "val2" ) ); + cpmap.insert( "X-key3", QString( "val3" ) ); + cpmap.insert( "X-key4", QString( "val4" ) ); + cpmap.insert( "X-key5", QString( "val5" ) ); + + CustomProperties cp1, cp2; + cp1.setCustomProperties( cpmap ); + cp1 = cp2; + QVERIFY( cp1 == cp2 ); + + CustomProperties cp3; + cp3.setCustomProperties( cp1.customProperties() ); + QVERIFY( cp1 == cp3 ); +} + +void CustomPropertiesTest::testEmpty() +{ + CustomProperties cp; + + QByteArray app( "KORG" ); + QByteArray key( "TEXT" ); + QString empty; + + cp.setCustomProperty( app, key, empty ); + QCOMPARE( cp.customProperty( app, key ), empty ); + + cp.removeCustomProperty( app, key ); + cp.setCustomProperty( app, key, empty ); + QCOMPARE( cp.customProperty( app, key ), empty ); + + key = "X-TEXT"; + cp.setNonKDECustomProperty( key, empty ); + QCOMPARE( cp.nonKDECustomProperty( key ), empty ); + + cp.removeNonKDECustomProperty( key ); + cp.setNonKDECustomProperty( key, empty ); + QCOMPARE( cp.nonKDECustomProperty( key ), empty ); +} diff --git a/kcal/tests/testcustomproperties.h b/kcal/tests/testcustomproperties.h new file mode 100644 index 000000000..f43e388a5 --- /dev/null +++ b/kcal/tests/testcustomproperties.h @@ -0,0 +1,37 @@ +/* + This file is part of the kcal library. + Copyright (c) 2009 Allen Winter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef TESTCUSTOMPROPERTIES_H +#define TESTCUSTOMPROPERTIES_H + +#include + +class CustomPropertiesTest : public QObject +{ + Q_OBJECT + private Q_SLOTS: + void testValidity(); + void testCompare(); + void testMapValidity(); + void testMapCompare(); + void testEmpty(); +}; + +#endif diff --git a/kcal/vcalformat.cpp b/kcal/vcalformat.cpp index 036f3f715..8c1a6dcb5 100644 --- a/kcal/vcalformat.cpp +++ b/kcal/vcalformat.cpp @@ -1,1674 +1,1689 @@ /* This file is part of the kcal library. Copyright (c) 1998 Preston Brown Copyright (c) 2001 Cornelius Schumacher This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** @file This file is part of the API for handling calendar data and defines the VCalFormat base class. This class implements the vCalendar format. It provides methods for loading/saving/converting vCalendar format data into the internal representation as Calendar and Incidences. @brief vCalendar format implementation. @author Preston Brown \ @author Cornelius Schumacher \ */ #include "vcalformat.h" #include "calendar.h" #include "versit/vcc.h" #include "versit/vobject.h" #include #include #include #include #include #include #include #include using namespace KCal; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class KCal::VCalFormat::Private { public: Calendar *mCalendar; Event::List mEventsRelate; // Events with relations Todo::List mTodosRelate; // To-dos with relations }; //@endcond VCalFormat::VCalFormat() : d( new KCal::VCalFormat::Private ) { } VCalFormat::~VCalFormat() { delete d; } bool VCalFormat::load( Calendar *calendar, const QString &fileName ) { d->mCalendar = calendar; clearException(); kDebug() << fileName; VObject *vcal = 0; // this is not necessarily only 1 vcal. Could be many vcals, or include // a vcard... vcal = Parse_MIME_FromFileName( const_cast( QFile::encodeName( fileName ).data() ) ); if ( !vcal ) { setException( new ErrorFormat( ErrorFormat::CalVersionUnknown ) ); return false; } // any other top-level calendar stuff should be added/initialized here // put all vobjects into their proper places populate( vcal ); // clean up from vcal API stuff cleanVObjects( vcal ); cleanStrTbl(); return true; } bool VCalFormat::save( Calendar *calendar, const QString &fileName ) { d->mCalendar = calendar; QString tmpStr; VObject *vcal, *vo; kDebug() << fileName; vcal = newVObject( VCCalProp ); // addPropValue(vcal,VCLocationProp, "0.0"); addPropValue( vcal, VCProdIdProp, productId().toLatin1() ); addPropValue( vcal, VCVersionProp, _VCAL_VERSION ); // TODO STUFF Todo::List todoList = d->mCalendar->rawTodos(); Todo::List::ConstIterator it; for ( it = todoList.constBegin(); it != todoList.constEnd(); ++it ) { vo = eventToVTodo( *it ); addVObjectProp( vcal, vo ); } // EVENT STUFF Event::List events = d->mCalendar->rawEvents(); Event::List::ConstIterator it2; for ( it2 = events.constBegin(); it2 != events.constEnd(); ++it2 ) { vo = eventToVEvent( *it2 ); addVObjectProp( vcal, vo ); } writeVObjectToFile( QFile::encodeName( fileName ).data(), vcal ); cleanVObjects( vcal ); cleanStrTbl(); if ( QFile::exists( fileName ) ) { return true; } else { return false; // error } return false; } bool VCalFormat::fromString( Calendar *calendar, const QString &string ) { return fromRawString( calendar, string.toUtf8() ); } bool VCalFormat::fromRawString( Calendar *calendar, const QByteArray &string ) { d->mCalendar = calendar; if ( !string.size() ) { return false; } VObject *vcal = Parse_MIME( string.data(), string.size() ); if ( !vcal ) { return false; } VObjectIterator i; VObject *curvo; initPropIterator( &i, vcal ); // we only take the first object. TODO: parse all incidences. do { curvo = nextVObject( &i ); } while ( strcmp( vObjectName( curvo ), VCEventProp ) && strcmp( vObjectName( curvo ), VCTodoProp ) ); if ( strcmp( vObjectName( curvo ), VCEventProp ) == 0 ) { Event *event = VEventToEvent( curvo ); calendar->addEvent( event ); } else { kDebug() << "Unknown object type."; deleteVObject( vcal ); return false; } deleteVObject( vcal ); return true; } QString VCalFormat::toString( Calendar *calendar ) { // TODO: Factor out VCalFormat::asString() d->mCalendar = calendar; VObject *vcal = newVObject( VCCalProp ); addPropValue( vcal, VCProdIdProp, CalFormat::productId().toLatin1() ); addPropValue( vcal, VCVersionProp, _VCAL_VERSION ); // TODO: Use all data. Event::List events = calendar->events(); if( events.isEmpty() ) { cleanVObject ( vcal ); return QString(); } Event *event = events.first(); if ( !event ) { cleanVObject ( vcal ); return QString(); } VObject *vevent = eventToVEvent( event ); addVObjectProp( vcal, vevent ); char *buf = writeMemVObject( 0, 0, vcal ); QString result( buf ); cleanVObject( vcal ); return result; } VObject *VCalFormat::eventToVTodo( const Todo *anEvent ) { VObject *vtodo; QString tmpStr; vtodo = newVObject( VCTodoProp ); // due date if ( anEvent->hasDueDate() ) { tmpStr = kDateTimeToISO( anEvent->dtDue(), !anEvent->allDay() ); addPropValue( vtodo, VCDueProp, tmpStr.toLocal8Bit() ); } // start date if ( anEvent->hasStartDate() ) { tmpStr = kDateTimeToISO( anEvent->dtStart(), !anEvent->allDay() ); addPropValue( vtodo, VCDTstartProp, tmpStr.toLocal8Bit() ); } // creation date tmpStr = kDateTimeToISO( anEvent->created() ); addPropValue( vtodo, VCDCreatedProp, tmpStr.toLocal8Bit() ); // unique id addPropValue( vtodo, VCUniqueStringProp, anEvent->uid().toLocal8Bit() ); // revision tmpStr.sprintf( "%i", anEvent->revision() ); addPropValue( vtodo, VCSequenceProp, tmpStr.toLocal8Bit() ); // last modification date tmpStr = kDateTimeToISO( anEvent->lastModified() ); addPropValue( vtodo, VCLastModifiedProp, tmpStr.toLocal8Bit() ); // organizer stuff // @TODO: How about the common name? if ( !anEvent->organizer().email().isEmpty() ) { tmpStr = "MAILTO:" + anEvent->organizer().email(); addPropValue( vtodo, ICOrganizerProp, tmpStr.toLocal8Bit() ); } // attendees if ( anEvent->attendeeCount() > 0 ) { Attendee::List::ConstIterator it; Attendee *curAttendee; for ( it = anEvent->attendees().begin(); it != anEvent->attendees().end(); ++it ) { curAttendee = *it; if ( !curAttendee->email().isEmpty() && !curAttendee->name().isEmpty() ) { tmpStr = "MAILTO:" + curAttendee->name() + " <" + curAttendee->email() + '>'; } else if ( curAttendee->name().isEmpty() ) { tmpStr = "MAILTO: " + curAttendee->email(); } else if ( curAttendee->email().isEmpty() ) { tmpStr = "MAILTO: " + curAttendee->name(); } else if ( curAttendee->name().isEmpty() && curAttendee->email().isEmpty() ) { kDebug() << "warning! this Event has an attendee w/o name or email!"; } VObject *aProp = addPropValue( vtodo, VCAttendeeProp, tmpStr.toLocal8Bit() ); addPropValue( aProp, VCRSVPProp, curAttendee->RSVP() ? "TRUE" : "FALSE" ); addPropValue( aProp, VCStatusProp, writeStatus( curAttendee->status() ) ); } } // description BL: if ( !anEvent->description().isEmpty() ) { VObject *d = addPropValue( vtodo, VCDescriptionProp, anEvent->description().toLocal8Bit() ); if ( anEvent->description().indexOf( '\n' ) != -1 ) { addPropValue( d, VCEncodingProp, VCQuotedPrintableProp ); } } // summary if ( !anEvent->summary().isEmpty() ) { addPropValue( vtodo, VCSummaryProp, anEvent->summary().toLocal8Bit() ); } // location if ( !anEvent->location().isEmpty() ) { addPropValue( vtodo, VCLocationProp, anEvent->location().toLocal8Bit() ); } // completed status // backward compatibility, KOrganizer used to interpret only these two values addPropValue( vtodo, VCStatusProp, anEvent->isCompleted() ? "COMPLETED" : "NEEDS_ACTION" ); // completion date if ( anEvent->hasCompletedDate() ) { tmpStr = kDateTimeToISO( anEvent->completed() ); addPropValue( vtodo, VCCompletedProp, tmpStr.toLocal8Bit() ); } // priority tmpStr.sprintf( "%i", anEvent->priority() ); addPropValue( vtodo, VCPriorityProp, tmpStr.toLocal8Bit() ); // related event if ( anEvent->relatedTo() ) { addPropValue( vtodo, VCRelatedToProp, anEvent->relatedTo()->uid().toLocal8Bit() ); } // categories const QStringList tmpStrList = anEvent->categories(); tmpStr = ""; QString catStr; QStringList::const_iterator its; for ( its = tmpStrList.constBegin(); its != tmpStrList.constEnd(); ++its ) { catStr = *its; if ( catStr[0] == ' ' ) { tmpStr += catStr.mid( 1 ); } else { tmpStr += catStr; } // this must be a ';' character as the vCalendar specification requires! // vcc.y has been hacked to translate the ';' to a ',' when the vcal is // read in. tmpStr += ';'; } if ( !tmpStr.isEmpty() ) { tmpStr.truncate( tmpStr.length() - 1 ); addPropValue( vtodo, VCCategoriesProp, tmpStr.toLocal8Bit() ); } // alarm stuff Alarm::List::ConstIterator it; for ( it = anEvent->alarms().begin(); it != anEvent->alarms().end(); ++it ) { Alarm *alarm = *it; if ( alarm->enabled() ) { VObject *a = addProp( vtodo, VCDAlarmProp ); tmpStr = kDateTimeToISO( alarm->time() ); addPropValue( a, VCRunTimeProp, tmpStr.toLocal8Bit() ); addPropValue( a, VCRepeatCountProp, "1" ); addPropValue( a, VCDisplayStringProp, "beep!" ); if ( alarm->type() == Alarm::Audio ) { a = addProp( vtodo, VCAAlarmProp ); addPropValue( a, VCRunTimeProp, tmpStr.toLocal8Bit() ); addPropValue( a, VCRepeatCountProp, "1" ); addPropValue( a, VCAudioContentProp, QFile::encodeName( alarm->audioFile() ) ); } else if ( alarm->type() == Alarm::Procedure ) { a = addProp( vtodo, VCPAlarmProp ); addPropValue( a, VCRunTimeProp, tmpStr.toLocal8Bit() ); addPropValue( a, VCRepeatCountProp, "1" ); addPropValue( a, VCProcedureNameProp, QFile::encodeName( alarm->programFile() ) ); } } } QString pilotId = anEvent->nonKDECustomProperty( KPilotIdProp ); if ( !pilotId.isEmpty() ) { // pilot sync stuff addPropValue( vtodo, KPilotIdProp, pilotId.toLocal8Bit() ); addPropValue( vtodo, KPilotStatusProp, anEvent->nonKDECustomProperty( KPilotStatusProp ).toLocal8Bit() ); } return vtodo; } VObject *VCalFormat::eventToVEvent( const Event *anEvent ) { VObject *vevent; QString tmpStr; vevent = newVObject( VCEventProp ); // start and end time tmpStr = kDateTimeToISO( anEvent->dtStart(), !anEvent->allDay() ); addPropValue( vevent, VCDTstartProp, tmpStr.toLocal8Bit() ); // events that have time associated but take up no time should // not have both DTSTART and DTEND. if ( anEvent->dtStart() != anEvent->dtEnd() ) { tmpStr = kDateTimeToISO( anEvent->dtEnd(), !anEvent->allDay() ); addPropValue( vevent, VCDTendProp, tmpStr.toLocal8Bit() ); } // creation date tmpStr = kDateTimeToISO( anEvent->created() ); addPropValue( vevent, VCDCreatedProp, tmpStr.toLocal8Bit() ); // unique id addPropValue( vevent, VCUniqueStringProp, anEvent->uid().toLocal8Bit() ); // revision tmpStr.sprintf( "%i", anEvent->revision() ); addPropValue( vevent, VCSequenceProp, tmpStr.toLocal8Bit() ); // last modification date tmpStr = kDateTimeToISO( anEvent->lastModified() ); addPropValue( vevent, VCLastModifiedProp, tmpStr.toLocal8Bit() ); // attendee and organizer stuff // TODO: What to do with the common name? if ( !anEvent->organizer().email().isEmpty() ) { tmpStr = "MAILTO:" + anEvent->organizer().email(); addPropValue( vevent, ICOrganizerProp, tmpStr.toLocal8Bit() ); } // TODO: Put this functionality into Attendee class if ( anEvent->attendeeCount() > 0 ) { Attendee::List::ConstIterator it; for ( it = anEvent->attendees().constBegin(); it != anEvent->attendees().constEnd(); ++it ) { Attendee *curAttendee = *it; if ( !curAttendee->email().isEmpty() && !curAttendee->name().isEmpty() ) { tmpStr = "MAILTO:" + curAttendee->name() + " <" + curAttendee->email() + '>'; } else if ( curAttendee->name().isEmpty() ) { tmpStr = "MAILTO: " + curAttendee->email(); } else if ( curAttendee->email().isEmpty() ) { tmpStr = "MAILTO: " + curAttendee->name(); } else if ( curAttendee->name().isEmpty() && curAttendee->email().isEmpty() ) { kDebug() << "warning! this Event has an attendee w/o name or email!"; } VObject *aProp = addPropValue( vevent, VCAttendeeProp, tmpStr.toLocal8Bit() ); addPropValue( aProp, VCRSVPProp, curAttendee->RSVP() ? "TRUE" : "FALSE" ); addPropValue( aProp, VCStatusProp, writeStatus( curAttendee->status() ) ); } } // recurrence rule stuff const Recurrence *recur = anEvent->recurrence(); if ( recur->recurs() ) { bool validRecur = true; QString tmpStr2; switch ( recur->recurrenceType() ) { case Recurrence::rDaily: tmpStr.sprintf( "D%i ", recur->frequency() ); break; case Recurrence::rWeekly: tmpStr.sprintf( "W%i ", recur->frequency() ); for ( int i = 0; i < 7; ++i ) { QBitArray days ( recur->days() ); if ( days.testBit(i) ) { tmpStr += dayFromNum( i ); } } break; case Recurrence::rMonthlyPos: { tmpStr.sprintf( "MP%i ", recur->frequency() ); // write out all rMonthPos's QList tmpPositions = recur->monthPositions(); for ( QList::ConstIterator posit = tmpPositions.constBegin(); posit != tmpPositions.constEnd(); ++posit ) { int pos = (*posit).pos(); tmpStr2.sprintf( "%i", ( pos > 0 ) ? pos : (-pos) ); if ( pos < 0 ) { tmpStr2 += "- "; } else { tmpStr2 += "+ "; } tmpStr += tmpStr2; tmpStr += dayFromNum( (*posit).day() - 1 ); } break; } case Recurrence::rMonthlyDay: { tmpStr.sprintf( "MD%i ", recur->frequency() ); // write out all rMonthDays; const QList tmpDays = recur->monthDays(); for ( QList::ConstIterator tmpDay = tmpDays.constBegin(); tmpDay != tmpDays.constEnd(); ++tmpDay ) { tmpStr2.sprintf( "%i ", *tmpDay ); tmpStr += tmpStr2; } break; } case Recurrence::rYearlyMonth: { tmpStr.sprintf( "YM%i ", recur->frequency() ); // write out all the months;' // TODO: Any way to write out the day within the month??? const QList months = recur->yearMonths(); for ( QList::ConstIterator mit = months.constBegin(); mit != months.constEnd(); ++mit ) { tmpStr2.sprintf( "%i ", *mit ); tmpStr += tmpStr2; } break; } case Recurrence::rYearlyDay: { tmpStr.sprintf( "YD%i ", recur->frequency() ); // write out all the rYearNums; const QList tmpDays = recur->yearDays(); for ( QList::ConstIterator tmpDay = tmpDays.begin(); tmpDay != tmpDays.end(); ++tmpDay ) { tmpStr2.sprintf( "%i ", *tmpDay ); tmpStr += tmpStr2; } break; } default: // TODO: Write rYearlyPos and arbitrary rules! kDebug() << "ERROR, it should never get here in eventToVEvent!"; validRecur = false; break; } // switch if ( recur->duration() > 0 ) { tmpStr2.sprintf( "#%i", recur->duration() ); tmpStr += tmpStr2; } else if ( recur->duration() == -1 ) { tmpStr += "#0"; // defined as repeat forever } else { tmpStr += kDateTimeToISO( recur->endDateTime(), false ); } // Only write out the rrule if we have a valid recurrence (i.e. a known // type in thee switch above) if ( validRecur ) { addPropValue( vevent, VCRRuleProp, tmpStr.toLocal8Bit() ); } } // event repeats // exceptions to recurrence DateList dateList = recur->exDates(); DateList::ConstIterator it; QString tmpStr2; for ( it = dateList.constBegin(); it != dateList.constEnd(); ++it ) { tmpStr = qDateToISO(*it) + ';'; tmpStr2 += tmpStr; } if ( !tmpStr2.isEmpty() ) { tmpStr2.truncate( tmpStr2.length() - 1 ); addPropValue( vevent, VCExpDateProp, tmpStr2.toLocal8Bit() ); } // description if ( !anEvent->description().isEmpty() ) { VObject *d = addPropValue( vevent, VCDescriptionProp, anEvent->description().toLocal8Bit() ); if ( anEvent->description().indexOf( '\n' ) != -1 ) { addPropValue( d, VCEncodingProp, VCQuotedPrintableProp ); } } // summary if ( !anEvent->summary().isEmpty() ) { addPropValue( vevent, VCSummaryProp, anEvent->summary().toLocal8Bit() ); } // location if ( !anEvent->location().isEmpty() ) { addPropValue( vevent, VCLocationProp, anEvent->location().toLocal8Bit() ); } // status // TODO: define Event status // addPropValue( vevent, VCStatusProp, anEvent->statusStr().toLocal8Bit() ); // secrecy const char *text = 0; switch ( anEvent->secrecy() ) { case Incidence::SecrecyPublic: text = "PUBLIC"; break; case Incidence::SecrecyPrivate: text = "PRIVATE"; break; case Incidence::SecrecyConfidential: text = "CONFIDENTIAL"; break; } if ( text ) { addPropValue( vevent, VCClassProp, text ); } // categories QStringList tmpStrList = anEvent->categories(); tmpStr = ""; QString catStr; for ( QStringList::const_iterator it = tmpStrList.constBegin(); it != tmpStrList.constEnd(); ++it ) { catStr = *it; if ( catStr[0] == ' ' ) { tmpStr += catStr.mid( 1 ); } else { tmpStr += catStr; } // this must be a ';' character as the vCalendar specification requires! // vcc.y has been hacked to translate the ';' to a ',' when the vcal is // read in. tmpStr += ';'; } if ( !tmpStr.isEmpty() ) { tmpStr.truncate( tmpStr.length() - 1 ); addPropValue( vevent, VCCategoriesProp, tmpStr.toLocal8Bit() ); } // attachments // TODO: handle binary attachments! Attachment::List attachments = anEvent->attachments(); Attachment::List::ConstIterator atIt; for ( atIt = attachments.constBegin(); atIt != attachments.constEnd(); ++atIt ) { addPropValue( vevent, VCAttachProp, (*atIt)->uri().toLocal8Bit() ); } // resources tmpStrList = anEvent->resources(); tmpStr = tmpStrList.join( ";" ); if ( !tmpStr.isEmpty() ) { addPropValue( vevent, VCResourcesProp, tmpStr.toLocal8Bit() ); } // alarm stuff Alarm::List::ConstIterator it2; for ( it2 = anEvent->alarms().constBegin(); it2 != anEvent->alarms().constEnd(); ++it2 ) { Alarm *alarm = *it2; if ( alarm->enabled() ) { VObject *a = addProp( vevent, VCDAlarmProp ); tmpStr = kDateTimeToISO( alarm->time() ); addPropValue( a, VCRunTimeProp, tmpStr.toLocal8Bit() ); addPropValue( a, VCRepeatCountProp, "1" ); addPropValue( a, VCDisplayStringProp, "beep!" ); if ( alarm->type() == Alarm::Audio ) { a = addProp( vevent, VCAAlarmProp ); addPropValue( a, VCRunTimeProp, tmpStr.toLocal8Bit() ); addPropValue( a, VCRepeatCountProp, "1" ); addPropValue( a, VCAudioContentProp, QFile::encodeName( alarm->audioFile() ) ); } if ( alarm->type() == Alarm::Procedure ) { a = addProp( vevent, VCPAlarmProp ); addPropValue( a, VCRunTimeProp, tmpStr.toLocal8Bit() ); addPropValue( a, VCRepeatCountProp, "1" ); addPropValue( a, VCProcedureNameProp, QFile::encodeName( alarm->programFile() ) ); } } } // priority tmpStr.sprintf( "%i", anEvent->priority() ); addPropValue( vevent, VCPriorityProp, tmpStr.toLocal8Bit() ); // transparency tmpStr.sprintf( "%i", anEvent->transparency() ); addPropValue( vevent, VCTranspProp, tmpStr.toLocal8Bit() ); // related event if ( anEvent->relatedTo() ) { addPropValue( vevent, VCRelatedToProp, anEvent->relatedTo()->uid().toLocal8Bit() ); } QString pilotId = anEvent->nonKDECustomProperty( KPilotIdProp ); if ( !pilotId.isEmpty() ) { // pilot sync stuff addPropValue( vevent, KPilotIdProp, pilotId.toLocal8Bit() ); addPropValue( vevent, KPilotStatusProp, anEvent->nonKDECustomProperty( KPilotStatusProp ).toLocal8Bit() ); } return vevent; } Todo *VCalFormat::VTodoToEvent( VObject *vtodo ) { VObject *vo; VObjectIterator voi; char *s; Todo *anEvent = new Todo; // creation date if ( ( vo = isAPropertyOf( vtodo, VCDCreatedProp ) ) != 0 ) { anEvent->setCreated( ISOToKDateTime( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); deleteStr( s ); } // unique id vo = isAPropertyOf( vtodo, VCUniqueStringProp ); // while the UID property is preferred, it is not required. We'll use the // default Event UID if none is given. if ( vo ) { anEvent->setUid( s = fakeCString( vObjectUStringZValue( vo ) ) ); deleteStr( s ); } // last modification date if ( ( vo = isAPropertyOf( vtodo, VCLastModifiedProp ) ) != 0 ) { anEvent->setLastModified( ISOToKDateTime( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); deleteStr( s ); } else { anEvent->setLastModified( KDateTime::currentUtcDateTime() ); } // organizer // if our extension property for the event's ORGANIZER exists, add it. if ( ( vo = isAPropertyOf( vtodo, ICOrganizerProp ) ) != 0 ) { anEvent->setOrganizer( s = fakeCString( vObjectUStringZValue( vo ) ) ); deleteStr( s ); } else { anEvent->setOrganizer( d->mCalendar->owner() ); } // attendees. initPropIterator( &voi, vtodo ); while ( moreIteration( &voi ) ) { vo = nextVObject( &voi ); if ( strcmp( vObjectName( vo ), VCAttendeeProp ) == 0 ) { Attendee *a; VObject *vp; s = fakeCString( vObjectUStringZValue( vo ) ); QString tmpStr = QString::fromLocal8Bit( s ); deleteStr( s ); tmpStr = tmpStr.simplified(); int emailPos1, emailPos2; if ( ( emailPos1 = tmpStr.indexOf( '<' ) ) > 0 ) { // both email address and name emailPos2 = tmpStr.lastIndexOf( '>' ); a = new Attendee( tmpStr.left( emailPos1 - 1 ), tmpStr.mid( emailPos1 + 1, emailPos2 - ( emailPos1 + 1 ) ) ); } else if ( tmpStr.indexOf( '@' ) > 0 ) { // just an email address a = new Attendee( 0, tmpStr ); } else { // just a name // WTF??? Replacing the spaces of a name and using this as email? QString email = tmpStr.replace( ' ', '.' ); a = new Attendee( tmpStr, email ); } // is there an RSVP property? if ( ( vp = isAPropertyOf( vo, VCRSVPProp ) ) != 0 ) { a->setRSVP( vObjectStringZValue( vp ) ); } // is there a status property? if ( ( vp = isAPropertyOf( vo, VCStatusProp ) ) != 0 ) { a->setStatus( readStatus( vObjectStringZValue( vp ) ) ); } // add the attendee anEvent->addAttendee( a ); } } // description for todo if ( ( vo = isAPropertyOf( vtodo, VCDescriptionProp ) ) != 0 ) { s = fakeCString( vObjectUStringZValue( vo ) ); anEvent->setDescription( QString::fromLocal8Bit( s ), Qt::mightBeRichText( s ) ); deleteStr( s ); } // summary if ( ( vo = isAPropertyOf( vtodo, VCSummaryProp ) ) ) { s = fakeCString( vObjectUStringZValue( vo ) ); anEvent->setSummary( QString::fromLocal8Bit( s ), Qt::mightBeRichText( s ) ); deleteStr( s ); } // location if ( ( vo = isAPropertyOf( vtodo, VCLocationProp ) ) != 0 ) { s = fakeCString( vObjectUStringZValue( vo ) ); anEvent->setLocation( QString::fromLocal8Bit( s ), Qt::mightBeRichText( s ) ); deleteStr( s ); } // completed // was: status if ( ( vo = isAPropertyOf( vtodo, VCStatusProp ) ) != 0 ) { s = fakeCString( vObjectUStringZValue( vo ) ); - if ( strcmp( s, "COMPLETED" ) == 0 ) { + if ( s && strcmp( s, "COMPLETED" ) == 0 ) { anEvent->setCompleted( true ); } else { anEvent->setCompleted( false ); } deleteStr( s ); } else { anEvent->setCompleted( false ); } // completion date if ( ( vo = isAPropertyOf( vtodo, VCCompletedProp ) ) != 0 ) { anEvent->setCompleted( ISOToKDateTime( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); deleteStr( s ); } // priority if ( ( vo = isAPropertyOf( vtodo, VCPriorityProp ) ) ) { - anEvent->setPriority( atoi( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); - deleteStr( s ); + s = fakeCString( vObjectUStringZValue( vo ) ); + if ( s ) { + anEvent->setPriority( atoi( s ) ); + deleteStr( s ); + } } // due date if ( ( vo = isAPropertyOf( vtodo, VCDueProp ) ) != 0 ) { anEvent->setDtDue( ISOToKDateTime( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); deleteStr( s ); anEvent->setHasDueDate( true ); } else { anEvent->setHasDueDate( false ); } // start time if ( ( vo = isAPropertyOf( vtodo, VCDTstartProp ) ) != 0 ) { anEvent->setDtStart( ISOToKDateTime( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); deleteStr( s ); anEvent->setHasStartDate( true ); } else { anEvent->setHasStartDate( false ); } // alarm stuff if ( ( vo = isAPropertyOf( vtodo, VCDAlarmProp ) ) ) { Alarm *alarm = anEvent->newAlarm(); VObject *a; if ( ( a = isAPropertyOf( vo, VCRunTimeProp ) ) ) { alarm->setTime( ISOToKDateTime( s = fakeCString( vObjectUStringZValue( a ) ) ) ); deleteStr( s ); } alarm->setEnabled( true ); if ( ( vo = isAPropertyOf( vtodo, VCPAlarmProp ) ) ) { if ( ( a = isAPropertyOf( vo, VCProcedureNameProp ) ) ) { s = fakeCString( vObjectUStringZValue( a ) ); alarm->setProcedureAlarm( QFile::decodeName( s ) ); deleteStr( s ); } } if ( ( vo = isAPropertyOf( vtodo, VCAAlarmProp ) ) ) { if ( ( a = isAPropertyOf( vo, VCAudioContentProp ) ) ) { s = fakeCString( vObjectUStringZValue( a ) ); alarm->setAudioAlarm( QFile::decodeName( s ) ); deleteStr( s ); } } } // related todo if ( ( vo = isAPropertyOf( vtodo, VCRelatedToProp ) ) != 0 ) { anEvent->setRelatedToUid( s = fakeCString( vObjectUStringZValue( vo ) ) ); deleteStr( s ); d->mTodosRelate.append( anEvent ); } // categories if ( ( vo = isAPropertyOf( vtodo, VCCategoriesProp ) ) != 0 ) { s = fakeCString( vObjectUStringZValue( vo ) ); QString categories = QString::fromLocal8Bit( s ); deleteStr( s ); QStringList tmpStrList = categories.split( ';' ); anEvent->setCategories( tmpStrList ); } /* PILOT SYNC STUFF */ if ( ( vo = isAPropertyOf( vtodo, KPilotIdProp ) ) ) { anEvent->setNonKDECustomProperty( KPilotIdProp, QString::fromLocal8Bit( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); deleteStr( s ); if ( ( vo = isAPropertyOf( vtodo, KPilotStatusProp ) ) ) { anEvent->setNonKDECustomProperty( KPilotStatusProp, QString::fromLocal8Bit( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); deleteStr( s ); } else { anEvent->setNonKDECustomProperty( KPilotStatusProp, QString::number( SYNCMOD ) ); } } return anEvent; } Event *VCalFormat::VEventToEvent( VObject *vevent ) { VObject *vo; VObjectIterator voi; char *s; Event *anEvent = new Event; // creation date if ( ( vo = isAPropertyOf( vevent, VCDCreatedProp ) ) != 0 ) { anEvent->setCreated( ISOToKDateTime( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); deleteStr( s ); } // unique id vo = isAPropertyOf( vevent, VCUniqueStringProp ); // while the UID property is preferred, it is not required. We'll use the // default Event UID if none is given. if ( vo ) { anEvent->setUid( s = fakeCString( vObjectUStringZValue( vo ) ) ); deleteStr( s ); } // revision // again NSCAL doesn't give us much to work with, so we improvise... + anEvent->setRevision( 0 ); if ( ( vo = isAPropertyOf( vevent, VCSequenceProp ) ) != 0 ) { - anEvent->setRevision( atoi( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); - deleteStr( s ); - } else { - anEvent->setRevision( 0 ); + s = fakeCString( vObjectUStringZValue( vo ) ); + if ( s ) { + anEvent->setRevision( atoi( s ) ); + deleteStr( s ); + } } // last modification date if ( ( vo = isAPropertyOf( vevent, VCLastModifiedProp ) ) != 0 ) { anEvent->setLastModified( ISOToKDateTime( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); deleteStr( s ); } else { anEvent->setLastModified( KDateTime::currentUtcDateTime() ); } // organizer // if our extension property for the event's ORGANIZER exists, add it. if ( ( vo = isAPropertyOf( vevent, ICOrganizerProp ) ) != 0 ) { // FIXME: Also use the full name, not just the email address anEvent->setOrganizer( s = fakeCString( vObjectUStringZValue( vo ) ) ); deleteStr( s ); } else { anEvent->setOrganizer( d->mCalendar->owner() ); } // deal with attendees. initPropIterator( &voi, vevent ); while ( moreIteration( &voi ) ) { vo = nextVObject( &voi ); if ( strcmp( vObjectName( vo ), VCAttendeeProp ) == 0 ) { Attendee *a; VObject *vp; s = fakeCString( vObjectUStringZValue( vo ) ); QString tmpStr = QString::fromLocal8Bit( s ); deleteStr( s ); tmpStr = tmpStr.simplified(); int emailPos1, emailPos2; if ( ( emailPos1 = tmpStr.indexOf( '<' ) ) > 0 ) { // both email address and name emailPos2 = tmpStr.lastIndexOf( '>' ); a = new Attendee( tmpStr.left( emailPos1 - 1 ), tmpStr.mid( emailPos1 + 1, emailPos2 - ( emailPos1 + 1 ) ) ); } else if ( tmpStr.indexOf( '@' ) > 0 ) { // just an email address a = new Attendee( 0, tmpStr ); } else { // just a name QString email = tmpStr.replace( ' ', '.' ); a = new Attendee( tmpStr, email ); } // is there an RSVP property? if ( ( vp = isAPropertyOf( vo, VCRSVPProp ) ) != 0 ) { a->setRSVP( vObjectStringZValue( vp ) ); } // is there a status property? if ( ( vp = isAPropertyOf( vo, VCStatusProp ) ) != 0 ) { a->setStatus( readStatus( vObjectStringZValue( vp ) ) ); } // add the attendee anEvent->addAttendee( a ); } } // This isn't strictly true. An event that doesn't have a start time // or an end time isn't all-day, it has an anchor in time but it doesn't // "take up" any time. /*if ((isAPropertyOf(vevent, VCDTstartProp) == 0) || (isAPropertyOf(vevent, VCDTendProp) == 0)) { anEvent->setAllDay(true); } else { }*/ anEvent->setAllDay( false ); // start time if ( ( vo = isAPropertyOf( vevent, VCDTstartProp ) ) != 0 ) { anEvent->setDtStart( ISOToKDateTime( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); deleteStr( s ); if ( anEvent->dtStart().time().isNull() ) { anEvent->setAllDay( true ); } } // stop time if ( ( vo = isAPropertyOf( vevent, VCDTendProp ) ) != 0 ) { anEvent->setDtEnd( ISOToKDateTime( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); deleteStr( s ); if ( anEvent->dtEnd().time().isNull() ) { anEvent->setAllDay( true ); } } // at this point, there should be at least a start or end time. // fix up for events that take up no time but have a time associated if ( !( vo = isAPropertyOf( vevent, VCDTstartProp ) ) ) { anEvent->setDtStart( anEvent->dtEnd() ); } if ( !( vo = isAPropertyOf( vevent, VCDTendProp ) ) ) { anEvent->setDtEnd( anEvent->dtStart() ); } /////////////////////////////////////////////////////////////////////////// // repeat stuff if ( ( vo = isAPropertyOf( vevent, VCRRuleProp ) ) != 0 ) { QString tmpStr = ( s = fakeCString( vObjectUStringZValue( vo ) ) ); deleteStr( s ); tmpStr.simplified(); tmpStr = tmpStr.toUpper(); // first, read the type of the recurrence int typelen = 1; uint type = Recurrence::rNone; if ( tmpStr.left(1) == "D" ) { type = Recurrence::rDaily; } else if ( tmpStr.left(1) == "W" ) { type = Recurrence::rWeekly; } else { typelen = 2; if ( tmpStr.left(2) == "MP" ) { type = Recurrence::rMonthlyPos; } else if ( tmpStr.left(2) == "MD" ) { type = Recurrence::rMonthlyDay; } else if ( tmpStr.left(2) == "YM" ) { type = Recurrence::rYearlyMonth; } else if ( tmpStr.left(2) == "YD" ) { type = Recurrence::rYearlyDay; } } if ( type != Recurrence::rNone ) { // Immediately after the type is the frequency int index = tmpStr.indexOf( ' ' ); int last = tmpStr.lastIndexOf( ' ' ) + 1; // find last entry int rFreq = tmpStr.mid( typelen, ( index - 1 ) ).toInt(); ++index; // advance to beginning of stuff after freq // Read the type-specific settings switch ( type ) { case Recurrence::rDaily: anEvent->recurrence()->setDaily(rFreq); break; case Recurrence::rWeekly: { QBitArray qba(7); QString dayStr; if ( index == last ) { // e.g. W1 #0 qba.setBit( anEvent->dtStart().date().dayOfWeek() - 1 ); } else { // e.g. W1 SU #0 while ( index < last ) { dayStr = tmpStr.mid( index, 3 ); int dayNum = numFromDay( dayStr ); - qba.setBit( dayNum ); + if ( dayNum >= 0 ) { + qba.setBit( dayNum ); + } index += 3; // advance to next day, or possibly "#" } } anEvent->recurrence()->setWeekly( rFreq, qba ); break; } case Recurrence::rMonthlyPos: { anEvent->recurrence()->setMonthly( rFreq ); QBitArray qba(7); short tmpPos; if ( index == last ) { // e.g. MP1 #0 tmpPos = anEvent->dtStart().date().day() / 7 + 1; if ( tmpPos == 5 ) { tmpPos = -1; } qba.setBit( anEvent->dtStart().date().dayOfWeek() - 1 ); anEvent->recurrence()->addMonthlyPos( tmpPos, qba ); } else { // e.g. MP1 1+ SU #0 while ( index < last ) { tmpPos = tmpStr.mid( index, 1 ).toShort(); index += 1; if ( tmpStr.mid( index, 1 ) == "-" ) { // convert tmpPos to negative tmpPos = 0 - tmpPos; } index += 2; // advance to day(s) while ( numFromDay( tmpStr.mid( index, 3 ) ) >= 0 ) { int dayNum = numFromDay( tmpStr.mid( index, 3 ) ); qba.setBit( dayNum ); index += 3; // advance to next day, or possibly pos or "#" } anEvent->recurrence()->addMonthlyPos( tmpPos, qba ); qba.detach(); qba.fill( false ); // clear out } // while != "#" } break; } case Recurrence::rMonthlyDay: anEvent->recurrence()->setMonthly( rFreq ); if( index == last ) { // e.g. MD1 #0 short tmpDay = anEvent->dtStart().date().day(); anEvent->recurrence()->addMonthlyDate( tmpDay ); } else { // e.g. MD1 3 #0 while ( index < last ) { int index2 = tmpStr.indexOf( ' ', index ); short tmpDay = tmpStr.mid( index, ( index2 - index ) ).toShort(); index = index2 - 1; if ( tmpStr.mid( index, 1 ) == "-" ) { tmpDay = 0 - tmpDay; } index += 2; // advance the index; anEvent->recurrence()->addMonthlyDate( tmpDay ); } // while != # } break; case Recurrence::rYearlyMonth: anEvent->recurrence()->setYearly( rFreq ); if ( index == last ) { // e.g. YM1 #0 short tmpMonth = anEvent->dtStart().date().month(); anEvent->recurrence()->addYearlyMonth( tmpMonth ); } else { // e.g. YM1 3 #0 while ( index < last ) { int index2 = tmpStr.indexOf( ' ', index ); short tmpMonth = tmpStr.mid( index, ( index2 - index ) ).toShort(); index = index2 + 1; anEvent->recurrence()->addYearlyMonth( tmpMonth ); } // while != # } break; case Recurrence::rYearlyDay: anEvent->recurrence()->setYearly( rFreq ); if ( index == last ) { // e.g. YD1 #0 short tmpDay = anEvent->dtStart().date().dayOfYear(); anEvent->recurrence()->addYearlyDay( tmpDay ); } else { // e.g. YD1 123 #0 while ( index < last ) { int index2 = tmpStr.indexOf( ' ', index ); short tmpDay = tmpStr.mid( index, ( index2 - index ) ).toShort(); index = index2 + 1; anEvent->recurrence()->addYearlyDay( tmpDay ); } // while != # } break; default: break; } // find the last field, which is either the duration or the end date index = last; if ( tmpStr.mid( index, 1 ) == "#" ) { // Nr of occurrences index++; int rDuration = tmpStr.mid( index, tmpStr.length() - index ).toInt(); if ( rDuration > 0 ) { anEvent->recurrence()->setDuration( rDuration ); } } else if ( tmpStr.indexOf( 'T', index ) != -1 ) { KDateTime rEndDate = ISOToKDateTime( tmpStr.mid( index, tmpStr.length() - index ) ); rEndDate.setDateOnly( true ); anEvent->recurrence()->setEndDateTime( rEndDate ); } // anEvent->recurrence()->dump(); } else { kDebug() << "we don't understand this type of recurrence!"; } // if known recurrence type } // repeats // recurrence exceptions if ( ( vo = isAPropertyOf( vevent, VCExpDateProp ) ) != 0 ) { s = fakeCString( vObjectUStringZValue( vo ) ); QStringList exDates = QString::fromLocal8Bit( s ).split( ',' ); QStringList::ConstIterator it; for ( it = exDates.constBegin(); it != exDates.constEnd(); ++it ) { anEvent->recurrence()->addExDate( ISOToQDate(*it) ); } deleteStr( s ); } // summary if ( ( vo = isAPropertyOf( vevent, VCSummaryProp ) ) ) { s = fakeCString( vObjectUStringZValue( vo ) ); anEvent->setSummary( QString::fromLocal8Bit( s ), Qt::mightBeRichText( s ) ); deleteStr( s ); } // description if ( ( vo = isAPropertyOf( vevent, VCDescriptionProp ) ) != 0 ) { s = fakeCString( vObjectUStringZValue( vo ) ); bool isRich = Qt::mightBeRichText( s ); if ( !anEvent->description().isEmpty() ) { anEvent->setDescription( anEvent->description() + '\n' + QString::fromLocal8Bit( s ), isRich ); } else { anEvent->setDescription( QString::fromLocal8Bit( s ), isRich ); } deleteStr( s ); } // location if ( ( vo = isAPropertyOf( vevent, VCLocationProp ) ) != 0 ) { s = fakeCString( vObjectUStringZValue( vo ) ); anEvent->setLocation( QString::fromLocal8Bit( s ), Qt::mightBeRichText( s ) ); deleteStr( s ); } // some stupid vCal exporters ignore the standard and use Description // instead of Summary for the default field. Correct for this. if ( anEvent->summary().isEmpty() && !( anEvent->description().isEmpty() ) ) { QString tmpStr = anEvent->description().simplified(); anEvent->setDescription( "" ); anEvent->setSummary( tmpStr ); } #if 0 // status if ( ( vo = isAPropertyOf( vevent, VCStatusProp ) ) != 0 ) { QString tmpStr( s = fakeCString( vObjectUStringZValue( vo ) ) ); deleteStr( s ); // TODO: Define Event status // anEvent->setStatus( tmpStr ); } else { // anEvent->setStatus( "NEEDS ACTION" ); } #endif // secrecy Incidence::Secrecy secrecy = Incidence::SecrecyPublic; if ( ( vo = isAPropertyOf( vevent, VCClassProp ) ) != 0 ) { s = fakeCString( vObjectUStringZValue( vo ) ); - if ( strcmp( s, "PRIVATE" ) == 0 ) { + if ( s && strcmp( s, "PRIVATE" ) == 0 ) { secrecy = Incidence::SecrecyPrivate; - } else if ( strcmp( s, "CONFIDENTIAL" ) == 0 ) { + } else if ( s && strcmp( s, "CONFIDENTIAL" ) == 0 ) { secrecy = Incidence::SecrecyConfidential; } deleteStr( s ); } anEvent->setSecrecy( secrecy ); // categories if ( ( vo = isAPropertyOf( vevent, VCCategoriesProp ) ) != 0 ) { s = fakeCString( vObjectUStringZValue( vo ) ); QString categories = QString::fromLocal8Bit( s ); deleteStr( s ); QStringList tmpStrList = categories.split( ',' ); anEvent->setCategories( tmpStrList ); } // attachments initPropIterator( &voi, vevent ); while ( moreIteration( &voi ) ) { vo = nextVObject( &voi ); if ( strcmp( vObjectName( vo ), VCAttachProp ) == 0 ) { s = fakeCString( vObjectUStringZValue( vo ) ); anEvent->addAttachment( new Attachment( QString( s ) ) ); deleteStr( s ); } } // resources if ( ( vo = isAPropertyOf( vevent, VCResourcesProp ) ) != 0 ) { QString resources = ( s = fakeCString( vObjectUStringZValue( vo ) ) ); deleteStr( s ); QStringList tmpStrList = resources.split( ';' ); anEvent->setResources( tmpStrList ); } // alarm stuff if ( ( vo = isAPropertyOf( vevent, VCDAlarmProp ) ) ) { Alarm *alarm = anEvent->newAlarm(); VObject *a; if ( ( a = isAPropertyOf( vo, VCRunTimeProp ) ) ) { alarm->setTime( ISOToKDateTime( s = fakeCString( vObjectUStringZValue( a ) ) ) ); deleteStr( s ); } alarm->setEnabled( true ); if ( ( vo = isAPropertyOf( vevent, VCPAlarmProp ) ) ) { if ( ( a = isAPropertyOf( vo, VCProcedureNameProp ) ) ) { s = fakeCString( vObjectUStringZValue( a ) ); alarm->setProcedureAlarm( QFile::decodeName( s ) ); deleteStr( s ); } } if ( ( vo = isAPropertyOf( vevent, VCAAlarmProp ) ) ) { if ( ( a = isAPropertyOf( vo, VCAudioContentProp ) ) ) { s = fakeCString( vObjectUStringZValue( a ) ); alarm->setAudioAlarm( QFile::decodeName( s ) ); deleteStr( s ); } } } // priority if ( ( vo = isAPropertyOf( vevent, VCPriorityProp ) ) ) { - anEvent->setPriority( atoi( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); - deleteStr( s ); + s = fakeCString( vObjectUStringZValue( vo ) ); + if ( s ) { + anEvent->setPriority( atoi( s ) ); + deleteStr( s ); + } } // transparency if ( ( vo = isAPropertyOf( vevent, VCTranspProp ) ) != 0 ) { - int i = atoi( s = fakeCString( vObjectUStringZValue( vo ) ) ); - anEvent->setTransparency( i == 1 ? Event::Transparent : Event::Opaque ); - deleteStr( s ); + s = fakeCString( vObjectUStringZValue( vo ) ); + if ( s ) { + int i = atoi( s ); + anEvent->setTransparency( i == 1 ? Event::Transparent : Event::Opaque ); + deleteStr( s ); + } } // related event if ( ( vo = isAPropertyOf( vevent, VCRelatedToProp ) ) != 0 ) { anEvent->setRelatedToUid( s = fakeCString( vObjectUStringZValue( vo ) ) ); deleteStr( s ); d->mEventsRelate.append( anEvent ); } /* PILOT SYNC STUFF */ if ( ( vo = isAPropertyOf( vevent, KPilotIdProp ) ) ) { anEvent->setNonKDECustomProperty( KPilotIdProp, QString::fromLocal8Bit( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); deleteStr( s ); if ( ( vo = isAPropertyOf( vevent, KPilotStatusProp ) ) ) { anEvent->setNonKDECustomProperty( KPilotStatusProp, QString::fromLocal8Bit( s = fakeCString( vObjectUStringZValue( vo ) ) ) ); deleteStr( s ); } else { anEvent->setNonKDECustomProperty( KPilotStatusProp, QString::number( SYNCMOD ) ); } } return anEvent; } QString VCalFormat::qDateToISO( const QDate &qd ) { QString tmpStr; if ( !qd.isValid() ) { return QString(); } tmpStr.sprintf( "%.2d%.2d%.2d", qd.year(), qd.month(), qd.day() ); return tmpStr; } QString VCalFormat::kDateTimeToISO( const KDateTime &dt, bool zulu ) { QString tmpStr; if ( !dt.isValid() ) { return QString(); } QDateTime tmpDT; if ( zulu ) { tmpDT = dt.toUtc().dateTime(); } else { tmpDT = dt.toTimeSpec( d->mCalendar->timeSpec() ).dateTime(); } tmpStr.sprintf( "%.2d%.2d%.2dT%.2d%.2d%.2d", tmpDT.date().year(), tmpDT.date().month(), tmpDT.date().day(), tmpDT.time().hour(), tmpDT.time().minute(), tmpDT.time().second() ); if ( zulu ) { tmpStr += 'Z'; } return tmpStr; } KDateTime VCalFormat::ISOToKDateTime( const QString &dtStr ) { QDate tmpDate; QTime tmpTime; QString tmpStr; int year, month, day, hour, minute, second; tmpStr = dtStr; year = tmpStr.left( 4 ).toInt(); month = tmpStr.mid( 4, 2 ).toInt(); day = tmpStr.mid( 6, 2 ).toInt(); hour = tmpStr.mid( 9, 2 ).toInt(); minute = tmpStr.mid( 11, 2 ).toInt(); second = tmpStr.mid( 13, 2 ).toInt(); tmpDate.setYMD( year, month, day ); tmpTime.setHMS( hour, minute, second ); if ( tmpDate.isValid() && tmpTime.isValid() ) { // correct for GMT if string is in Zulu format if ( dtStr.at( dtStr.length() - 1 ) == 'Z' ) { return KDateTime( tmpDate, tmpTime, KDateTime::UTC ); } else { return KDateTime( tmpDate, tmpTime, d->mCalendar->timeSpec() ); } } else { return KDateTime(); } } QDate VCalFormat::ISOToQDate( const QString &dateStr ) { int year, month, day; year = dateStr.left( 4 ).toInt(); month = dateStr.mid( 4, 2 ).toInt(); day = dateStr.mid( 6, 2 ).toInt(); return QDate( year, month, day ); } // take a raw vcalendar (i.e. from a file on disk, clipboard, etc. etc. // and break it down from it's tree-like format into the dictionary format // that is used internally in the VCalFormat. void VCalFormat::populate( VObject *vcal ) { // this function will populate the caldict dictionary and other event // lists. It turns vevents into Events and then inserts them. VObjectIterator i; VObject *curVO, *curVOProp; Event *anEvent; if ( ( curVO = isAPropertyOf( vcal, ICMethodProp ) ) != 0 ) { char *methodType = 0; methodType = fakeCString( vObjectUStringZValue( curVO ) ); kDebug() << "This calendar is an iTIP transaction of type '" << methodType << "'"; deleteStr( methodType ); } // warn the user that we might have trouble reading non-known calendar. if ( ( curVO = isAPropertyOf( vcal, VCProdIdProp ) ) != 0 ) { char *s = fakeCString( vObjectUStringZValue( curVO ) ); - if ( strcmp( productId().toLocal8Bit(), s ) != 0 ) { + if ( !s || strcmp( productId().toLocal8Bit(), s ) != 0 ) { kDebug() << "This vCalendar file was not created by KOrganizer or" << "any other product we support. Loading anyway..."; } setLoadedProductId( s ); deleteStr( s ); } // warn the user we might have trouble reading this unknown version. if ( ( curVO = isAPropertyOf( vcal, VCVersionProp ) ) != 0 ) { char *s = fakeCString( vObjectUStringZValue( curVO ) ); - if ( strcmp( _VCAL_VERSION, s ) != 0 ) { + if ( !s || strcmp( _VCAL_VERSION, s ) != 0 ) { kDebug() << "This vCalendar file has version" << s << "We only support" << _VCAL_VERSION; } deleteStr( s ); } #if 0 // set the time zone (this is a property of the view, so just discard!) if ( ( curVO = isAPropertyOf( vcal, VCTimeZoneProp ) ) != 0 ) { char *s = fakeCString( vObjectUStringZValue( curVO ) ); d->mCalendar->setTimeZone( s ); deleteStr( s ); } #endif // Store all events with a relatedTo property in a list for post-processing d->mEventsRelate.clear(); d->mTodosRelate.clear(); initPropIterator( &i, vcal ); // go through all the vobjects in the vcal while ( moreIteration( &i ) ) { curVO = nextVObject( &i ); /************************************************************************/ // now, check to see that the object is an event or todo. if ( strcmp( vObjectName( curVO ), VCEventProp ) == 0 ) { if ( ( curVOProp = isAPropertyOf( curVO, KPilotStatusProp ) ) != 0 ) { char *s; s = fakeCString( vObjectUStringZValue( curVOProp ) ); // check to see if event was deleted by the kpilot conduit - if ( atoi( s ) == SYNCDEL ) { + if ( s ) { + if ( atoi( s ) == SYNCDEL ) { + deleteStr( s ); + kDebug() << "skipping pilot-deleted event"; + goto SKIP; + } deleteStr( s ); - kDebug() << "skipping pilot-deleted event"; - goto SKIP; } - deleteStr( s ); } // this code checks to see if we are trying to read in an event // that we already find to be in the calendar. If we find this // to be the case, we skip the event. if ( ( curVOProp = isAPropertyOf( curVO, VCUniqueStringProp ) ) != 0 ) { char *s = fakeCString( vObjectUStringZValue( curVOProp ) ); QString tmpStr( s ); deleteStr( s ); if ( d->mCalendar->incidence( tmpStr ) ) { goto SKIP; } } if ( ( !( curVOProp = isAPropertyOf( curVO, VCDTstartProp ) ) ) && ( !( curVOProp = isAPropertyOf( curVO, VCDTendProp ) ) ) ) { kDebug() << "found a VEvent with no DTSTART and no DTEND! Skipping..."; goto SKIP; } anEvent = VEventToEvent( curVO ); // we now use addEvent instead of insertEvent so that the // signal/slot get connected. if ( anEvent ) { if ( anEvent->dtStart().isValid() && anEvent->dtEnd().isValid() ) { d->mCalendar->addEvent( anEvent ); } } else { // some sort of error must have occurred while in translation. goto SKIP; } } else if ( strcmp( vObjectName( curVO ), VCTodoProp ) == 0 ) { Todo *aTodo = VTodoToEvent( curVO ); Todo *old = d->mCalendar->todo( aTodo->uid() ); if ( old ) { d->mCalendar->deleteTodo( old ); d->mTodosRelate.removeAll( old ); } d->mCalendar->addTodo( aTodo ); } else if ( ( strcmp( vObjectName( curVO ), VCVersionProp ) == 0 ) || ( strcmp( vObjectName( curVO ), VCProdIdProp ) == 0 ) || ( strcmp( vObjectName( curVO ), VCTimeZoneProp ) == 0 ) ) { // do nothing, we know these properties and we want to skip them. // we have either already processed them or are ignoring them. ; } else { kDebug() << "Ignoring unknown vObject \"" << vObjectName(curVO) << "\""; } SKIP: ; } // while // Post-Process list of events with relations, put Event objects in relation Event::List::ConstIterator eIt; for ( eIt = d->mEventsRelate.constBegin(); eIt != d->mEventsRelate.constEnd(); ++eIt ) { (*eIt)->setRelatedTo( d->mCalendar->incidence( (*eIt)->relatedToUid() ) ); } Todo::List::ConstIterator tIt; for ( tIt = d->mTodosRelate.constBegin(); tIt != d->mTodosRelate.constEnd(); ++tIt ) { (*tIt)->setRelatedTo( d->mCalendar->incidence( (*tIt)->relatedToUid() ) ); } } const char *VCalFormat::dayFromNum( int day ) { const char *days[7] = { "MO ", "TU ", "WE ", "TH ", "FR ", "SA ", "SU " }; return days[day]; } int VCalFormat::numFromDay( const QString &day ) { if ( day == "MO " ) { return 0; } if ( day == "TU " ) { return 1; } if ( day == "WE " ) { return 2; } if ( day == "TH " ) { return 3; } if ( day == "FR " ) { return 4; } if ( day == "SA " ) { return 5; } if ( day == "SU " ) { return 6; } return -1; // something bad happened. :) } Attendee::PartStat VCalFormat::readStatus( const char *s ) const { QString statStr = s; statStr = statStr.toUpper(); Attendee::PartStat status; if ( statStr == "X-ACTION" ) { status = Attendee::NeedsAction; } else if ( statStr == "NEEDS ACTION" ) { status = Attendee::NeedsAction; } else if ( statStr == "ACCEPTED" ) { status = Attendee::Accepted; } else if ( statStr == "SENT" ) { status = Attendee::NeedsAction; } else if ( statStr == "TENTATIVE" ) { status = Attendee::Tentative; } else if ( statStr == "CONFIRMED" ) { status = Attendee::Accepted; } else if ( statStr == "DECLINED" ) { status = Attendee::Declined; } else if ( statStr == "COMPLETED" ) { status = Attendee::Completed; } else if ( statStr == "DELEGATED" ) { status = Attendee::Delegated; } else { kDebug() << "error setting attendee mStatus, unknown mStatus!"; status = Attendee::NeedsAction; } return status; } QByteArray VCalFormat::writeStatus( Attendee::PartStat status ) const { switch( status ) { default: case Attendee::NeedsAction: return "NEEDS ACTION"; break; case Attendee::Accepted: return "ACCEPTED"; break; case Attendee::Declined: return "DECLINED"; break; case Attendee::Tentative: return "TENTATIVE"; break; case Attendee::Delegated: return "DELEGATED"; break; case Attendee::Completed: return "COMPLETED"; break; case Attendee::InProcess: return "NEEDS ACTION"; break; } } diff --git a/kholidays/holidays/CMakeLists.txt b/kholidays/holidays/CMakeLists.txt index 5c0e63adf..42c5eceda 100644 --- a/kholidays/holidays/CMakeLists.txt +++ b/kholidays/holidays/CMakeLists.txt @@ -1,56 +1,57 @@ install( FILES holiday_ar holiday_at holiday_au holiday_bavarian holiday_BelgiumDutch holiday_BelgiumFrench holiday_BelgiumWalloon holiday_bg holiday_br holiday_ca holiday_catalan holiday_ch holiday_cl holiday_co holiday_cz holiday_de holiday_dk holiday_ee holiday_es holiday_fi holiday_fr holiday_frswiss holiday_gb holiday_gr holiday_gt holiday_hu holiday_ie holiday_il + holiday_in holiday_is holiday_it holiday_jm holiday_jp holiday_lt holiday_mx holiday_nl holiday_no holiday_nz holiday_pl holiday_pt holiday_py holiday_quebec holiday_ro holiday_ru holiday_se holiday_si holiday_sk holiday_Suedtirol holiday_th holiday_ua holiday_us holiday_uy holiday_za DESTINATION ${DATA_INSTALL_DIR}/libkholidays) diff --git a/kholidays/holidays/holiday_in b/kholidays/holidays/holiday_in new file mode 100644 index 000000000..89c867cee --- /dev/null +++ b/kholidays/holidays/holiday_in @@ -0,0 +1,36 @@ +: +: IN holiday file. Copy to ~/.holiday +: These were taken from supremecourtofindia webpage, +: and Indian Embassy london page +: Since different states have small changes it is very difficult to +: be accurate and cover everything. +: +: Author: Karthik. If you need any clarification please contact me +: molecularbiophysics@yahoo.com +: +: Most of these are Federal holidays +: Note: Only the fixed holidays are available. Some celebrations depend on +: special events, eg: Eid al-Fitr which depend on the ramadan end date, +: Holi, Divali and Raksh Bandan which depend on the moon phase (not handled +: in kholidays) + +small "New Year holiday" on january 1 +small "Muharram" on january 10 +small "Republic Day" on january 26 +: small "Milad-un-Nabi" on march 9 +: small "Holi -Festival of Colours" on march 14 +: small "Ram navami" on april 3 +: small "Mahavir Jayanthi" on april 7 +: small "Good Friday" on april 10 +: small "Buddha Purnima" on may 9 +: small "Raksha Bandhan" on august 5 +: small "Janmashtami" on august 14 +small "Independence Day" on august 15 +: small "Idu'l Fitr" on september 21 +: small "Dussehra(Vijaya Dashami)" on september 28 +small "Mahatma Gandhi's Birthday" on october 2 +: small "Diwali" on October 17 +: small "Guru Nanak's Birthday" November 2 +: small "Bakrid" November 28 +small "Christmas" December 25 +: small "Muharram" December 28 diff --git a/kpimtextedit/richtextbuilders/tests/CMakeLists.txt b/kpimtextedit/richtextbuilders/tests/CMakeLists.txt index 2e3b9a52a..5d4ce3ca8 100644 --- a/kpimtextedit/richtextbuilders/tests/CMakeLists.txt +++ b/kpimtextedit/richtextbuilders/tests/CMakeLists.txt @@ -1,26 +1,26 @@ ENABLE_TESTING() FIND_PACKAGE(KDE4 REQUIRED) INCLUDE_DIRECTORIES( ${KDE4_INCLUDES} ${CMAKE_CURRENT_BINARY_DIR} ) set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) SET(krichtexteditorSources ../kmarkupdirector.cpp ../kmarkupdirector_p.cpp ../khtmlbuilder.cpp ../kplaintextmarkupbuilder.cpp ) MACRO(KDEUI_UNIT_TESTS) FOREACH(_testname ${ARGN}) kde4_add_unit_test(${_testname} ${_testname}.cpp ${krichtexteditorSources} ) - target_link_libraries(${_testname} ${KDE4_KDEUI_LIBS} ${QT_QTTEST_LIBRARY} ${QT_QTXML_LIBRARY} ${KDEWIN32_LIBRARIES} ) + target_link_libraries(${_testname} ${KDE4_KDEUI_LIBS} ${QT_QTTEST_LIBRARY} ${QT_QTXML_LIBRARY} ${KDEWIN_LIBRARIES} ) ENDFOREACH(_testname) ENDMACRO(KDEUI_UNIT_TESTS) KDEUI_UNIT_TESTS( htmlbuildertest plainmarkupbuildertest ) diff --git a/kpimtextedit/tests/textedittest.cpp b/kpimtextedit/tests/textedittest.cpp index e86ab5a48..cdcf228f4 100644 --- a/kpimtextedit/tests/textedittest.cpp +++ b/kpimtextedit/tests/textedittest.cpp @@ -1,324 +1,380 @@ /* Copyright (c) 2009 Thomas McGuire This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "qtest_kde.h" #include "textedittest.h" #include "../textedit.h" #include "kmime/kmime_codecs.h" #include #include #include #include using namespace KPIMTextEdit; QTEST_KDEMAIN( TextEditTester, GUI ) void TextEditTester::testFormattingUsed() { // This method tries to test everything that krichtextedit makes available, so // we can sure that in KMail, when the user uses some formatting, the mail is actually // sent as HTML mail TextEdit textEdit; QVERIFY( !textEdit.isFormattingUsed() ); // Insert some text. QTextCursor cursor( textEdit.document() ); cursor.insertText( QLatin1String( "Hello World!!" ) ); QVERIFY( !textEdit.isFormattingUsed() ); cursor.setPosition( 1 ); textEdit.setTextCursor( cursor ); // // Test link // QString someUrl = QLatin1String( "www.test.de" ); QString altText = QLatin1String( "Hello" ); textEdit.updateLink( someUrl, altText ); QVERIFY( textEdit.isFormattingUsed() ); QCOMPARE( textEdit.currentLinkUrl(), someUrl ); QCOMPARE( textEdit.currentLinkText(), altText ); cursor.setPosition( 1 ); textEdit.setTextCursor( cursor ); textEdit.updateLink( QString(), QLatin1String( "Hello" ) ); QVERIFY( textEdit.currentLinkUrl().isEmpty() ); QVERIFY( !textEdit.currentLinkText().isEmpty() ); QVERIFY( !textEdit.isFormattingUsed() ); // // Test alignment // cursor.setPosition( 1 ); textEdit.setTextCursor( cursor ); textEdit.alignRight(); QVERIFY( textEdit.isFormattingUsed() ); QCOMPARE( textEdit.alignment(), Qt::AlignRight ); textEdit.alignLeft(); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.alignCenter(); QCOMPARE( textEdit.alignment(), Qt::AlignHCenter ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.alignJustify(); QCOMPARE( textEdit.alignment(), Qt::AlignJustify ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.alignLeft(); QCOMPARE( textEdit.alignment(), Qt::AlignLeft ); QVERIFY( !textEdit.isFormattingUsed() ); // // Test lists // textEdit.setListStyle( QTextListFormat::ListCircle ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setListStyle( 0 ); QVERIFY( !textEdit.isFormattingUsed() ); // // Test font attributes // textEdit.setFontFamily( QLatin1String( "Times" ) ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setFontFamily( textEdit.document()->defaultFont().family() ); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.setFontSize( 48 ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setFontSize( textEdit.document()->defaultFont().pointSize() ); QVERIFY( !textEdit.isFormattingUsed() ); QFont myFont = textEdit.document()->defaultFont(); myFont.setStyle( QFont::StyleOblique ); textEdit.setFont( myFont ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setFont( textEdit.document()->defaultFont() ); QVERIFY( !textEdit.isFormattingUsed() ); // // Test bold, italic, underline and strikeout // textEdit.setTextBold( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextBold( false ); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.setTextUnderline( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextUnderline( false ); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.setTextItalic( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextItalic( false ); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.setTextStrikeOut( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextStrikeOut( false ); QVERIFY( !textEdit.isFormattingUsed() ); // // Color // QColor oldForeground = textEdit.document()->firstBlock().charFormat().foreground().color(); textEdit.setTextForegroundColor( Qt::red ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextForegroundColor( oldForeground ); QVERIFY( !textEdit.isFormattingUsed() ); QColor oldBackground = textEdit.document()->firstBlock().charFormat().background().color(); textEdit.setTextBackgroundColor( Qt::red ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextBackgroundColor( oldBackground ); QVERIFY( !textEdit.isFormattingUsed() ); // // Horizontal rule // textEdit.insertHorizontalRule(); QVERIFY( textEdit.isFormattingUsed() ); // No way to easily remove the horizontal line, so clear the text edit and start over textEdit.clear(); cursor.insertText( QLatin1String( "Hello World!!" ) ); QVERIFY( !textEdit.isFormattingUsed() ); cursor.setPosition( 1 ); textEdit.setTextCursor( cursor ); // // Sub and superscript // textEdit.setTextSuperScript( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextSuperScript( false ); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.setTextSubScript( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextSubScript( false ); QVERIFY( !textEdit.isFormattingUsed() ); // // Image // QString imagePath = KIconLoader::global()->iconPath( QLatin1String( "folder-new" ), KIconLoader::Small, false ); textEdit.addImage( imagePath ); QVERIFY( textEdit.isFormattingUsed() ); cursor = textEdit.textCursor(); cursor.movePosition( QTextCursor::Left, QTextCursor::KeepAnchor, 1 ); cursor.removeSelectedText(); QVERIFY( !textEdit.isFormattingUsed() ); } void TextEditTester::testQuoting() { TextEdit edit; QVERIFY( edit.isLineQuoted( QLatin1String( "> Hello" ) ) ); QVERIFY( edit.isLineQuoted( QLatin1String( ">Hello" ) ) ); QVERIFY( !edit.isLineQuoted( QLatin1String( "Hello" ) ) ); QCOMPARE( edit.quoteLength( QLatin1String( "Hello" ) ), 0 ); QCOMPARE( edit.quoteLength( QLatin1String( ">Hello" ) ), 1 ); QCOMPARE( edit.quoteLength( QLatin1String( "> Hello" ) ), 2 ); QCOMPARE( edit.quoteLength( QLatin1String( ">>>Hello" ) ), 3 ); QCOMPARE( edit.quoteLength( QLatin1String( "> > > Hello" ) ), 6 ); QCOMPARE( edit.quoteLength( QLatin1String( "|Hello" ) ), 1 ); QCOMPARE( edit.quoteLength( QLatin1String( "| |Hello" ) ), 3 ); } void TextEditTester::testCleanText() { TextEdit edit; QLatin1String html( "Heelllo World
      Bye!" ); QLatin1String plain( "Heelllo World\nBye!" ); edit.setTextOrHtml( html ); edit.addImage( KIconLoader::global()->iconPath( QLatin1String( "folder-new" ), KIconLoader::Small, false ) ); QVERIFY( edit.textMode() == TextEdit::Rich ); QCOMPARE( edit.toCleanPlainText(), plain ); edit.show(); // < otherwise toWrappedPlainText can't work, it needs a layout QCOMPARE( edit.toWrappedPlainText(), plain ); } void TextEditTester::testEnter_data() { QTest::addColumn("initalText"); QTest::addColumn("expectedText"); QTest::addColumn("cursorPos"); QTest::newRow( "" ) << QString::fromAscii( "> Hello World" ) << QString::fromAscii( "> Hello \n> World" ) << 8; QTest::newRow( "" ) << QString::fromAscii( "Hello World" ) << QString::fromAscii( "Hello \nWorld" ) << 6; QTest::newRow( "" ) << QString::fromAscii( "> Hello World" ) << QString::fromAscii( "> Hello World\n" ) << 13; QTest::newRow( "" ) << QString::fromAscii( ">Hello World" ) << QString::fromAscii( ">Hello \n>World" ) << 7; QTest::newRow( "" ) << QString::fromAscii( "> > Hello World" ) << QString::fromAscii( "> > Hello \n> > World" ) << 10; QTest::newRow( "" ) << QString::fromAscii( "| | Hello World" ) << QString::fromAscii( "| | Hello \n| | World" ) << 10; } void TextEditTester::testEnter() { QFETCH( QString, initalText ); QFETCH( QString, expectedText ); QFETCH( int, cursorPos ); TextEdit edit; edit.setPlainText( initalText ); QTextCursor textCursor( edit.document() ); textCursor.setPosition( cursorPos ); edit.setTextCursor( textCursor ); QTest::keyClick( &edit, Qt::Key_Return ); QCOMPARE( edit.toPlainText(), expectedText ); } void TextEditTester::testImages() { TextEdit edit; QString image1Path = KIconLoader::global()->iconPath( QLatin1String( "folder-new" ), KIconLoader::Small, false ); QString image2Path = KIconLoader::global()->iconPath( QLatin1String( "arrow-up" ), KIconLoader::Small, false ); // Add one image, check that embeddedImages() returns the right stuff edit.addImage( image1Path ); KPIMTextEdit::ImageList images = edit.embeddedImages(); QCOMPARE( images.size(), 1 ); EmbeddedImage *image = images.first().data(); QCOMPARE( image->imageName, QString::fromAscii( "folder-new.png" ) ); // Also check that it loads the correct image QImage diskImage( image1Path ); QBuffer buffer; buffer.open( QIODevice::WriteOnly ); diskImage.save( &buffer, "PNG" ); QByteArray encodedImage = KMime::Codec::codecForName( "base64" )->encode( buffer.buffer() ); QCOMPARE( image->image, encodedImage ); // No image should be there after clearing edit.clear(); QVERIFY( edit.embeddedImages().isEmpty() ); // Check that manually removing the image also empties the image list edit.addImage( image1Path ); QCOMPARE( edit.embeddedImages().size(), 1 ); QTextCursor cursor = edit.textCursor(); cursor.setPosition( 0, QTextCursor::MoveAnchor ); cursor.movePosition( QTextCursor::Right, QTextCursor::KeepAnchor, 1 ); cursor.removeSelectedText(); QVERIFY( edit.embeddedImages().isEmpty() ); // Check that adding the identical image two times only adds the image once edit.addImage( image1Path ); edit.addImage( image1Path ); QCOMPARE( edit.embeddedImages().size(), 1 ); // Another different image added, and we should have two images edit.clear(); edit.addImage( image1Path ); edit.addImage( image2Path ); images = edit.embeddedImages(); QCOMPARE( images.size(), 2 ); KPIMTextEdit::EmbeddedImage *image1 = images.first().data(); KPIMTextEdit::EmbeddedImage *image2 = images.last().data(); QCOMPARE( image1->imageName, QString::fromAscii( "folder-new2.png" ) ); // ### FIXME: should be folder-new.png, but QTextEdit provides no way to remove cached resources! QCOMPARE( image2->imageName, QString::fromAscii( "arrow-up.png" ) ); QVERIFY( image1->contentID != image2->contentID ); } void TextEditTester::testImageHtmlCode() { TextEdit edit; QString image1Path = KIconLoader::global()->iconPath( QLatin1String( "folder-new" ), KIconLoader::Small, false ); QString image2Path = KIconLoader::global()->iconPath( QLatin1String( "arrow-up" ), KIconLoader::Small, false ); edit.addImage( image1Path ); edit.addImage( image2Path ); KPIMTextEdit::ImageList images = edit.embeddedImages(); QCOMPARE( images.size(), 2 ); KPIMTextEdit::EmbeddedImage *image1 = images.first().data(); KPIMTextEdit::EmbeddedImage *image2 = images.last().data(); QString startHtml = QLatin1String( "BlaBlub" ); QString endHtml = QString( QLatin1String( "BlaBlub" ) ) .arg( image2->contentID ).arg( image1->contentID ); QCOMPARE( TextEdit::imageNamesToContentIds( startHtml.toAscii(), images ), endHtml.toAscii() ); } + +void TextEditTester::testDeleteLine_data() +{ + QTest::addColumn("initalText"); + QTest::addColumn("expectedText"); + QTest::addColumn("cursorPos"); + + QTest::newRow( "" ) << QString::fromAscii( "line1\nline2\nline3" ) + << QString::fromAscii( "line1\nline3" ) + << 6; + QTest::newRow( "" ) << QString::fromAscii( "line1\nline2\nline3" ) + << QString::fromAscii( "line2\nline3" ) + << 5; + QTest::newRow( "" ) << QString::fromAscii( "line1\nline2\nline3" ) + << QString::fromAscii( "line1\nline3" ) + << 11; + QTest::newRow( "" ) << QString::fromAscii( "line1\nline2\nline3" ) + << QString::fromAscii( "line2\nline3" ) + << 0; + QTest::newRow( "" ) << QString::fromAscii( "line1\nline2\nline3" ) + << QString::fromAscii( "line1\nline2" ) + << 17; + QTest::newRow( "" ) << QString::fromAscii( "line1" ) + << QString::fromAscii( "" ) + << 0; + QTest::newRow( "" ) << QString::fromAscii( "line1" ) + << QString::fromAscii( "" ) + << 5; + + // Now, test deletion with word wrapping. The line with the Ms is so long that it will get wrapped + QTest::newRow( "" ) << QString::fromAscii( "line1\nMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3" ) + << QString::fromAscii( "line1\nMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3" ) + << 6; + QTest::newRow( "" ) << QString::fromAscii( "line1\nMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3" ) + << QString::fromAscii( "line1\nMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3" ) + << 13; +} + +void TextEditTester::testDeleteLine() +{ + QFETCH( QString, initalText ); + QFETCH( QString, expectedText ); + QFETCH( int, cursorPos ); + + TextEdit edit; + edit.setPlainText( initalText ); + QTextCursor cursor = edit.textCursor(); + cursor.setPosition( cursorPos ); + edit.setTextCursor( cursor ); + + edit.show(); // we need a layout for this to work + + edit.deleteCurrentLine(); + QCOMPARE( edit.toPlainText(), expectedText ); +} + diff --git a/kpimtextedit/tests/textedittest.h b/kpimtextedit/tests/textedittest.h index 523bfe4b8..dc7a86b90 100644 --- a/kpimtextedit/tests/textedittest.h +++ b/kpimtextedit/tests/textedittest.h @@ -1,39 +1,41 @@ /* Copyright (c) 2009 Thomas McGuire This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef TEXTEDITTEST_H #define TEXTEDITTEST_H #include class TextEditTester : public QObject { Q_OBJECT private slots: void testFormattingUsed(); void testQuoting(); void testCleanText(); void testEnter(); void testEnter_data(); void testImages(); void testImageHtmlCode(); + void testDeleteLine(); + void testDeleteLine_data(); }; #endif diff --git a/kpimtextedit/textedit.cpp b/kpimtextedit/textedit.cpp index ede74e94f..5101fab7d 100644 --- a/kpimtextedit/textedit.cpp +++ b/kpimtextedit/textedit.cpp @@ -1,607 +1,666 @@ /* Copyright (c) 2009 Thomas McGuire Based on KMail and libkdepim code by: Copyright 2007 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "textedit.h" #include "emailquotehighlighter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace KPIMTextEdit { class TextEditPrivate { public: TextEditPrivate( TextEdit *parent ) - : q( parent ), + : actionAddImage( 0 ), + actionDeleteLine( 0 ), + q( parent ), imageSupportEnabled( false ) { } /** * Helper function for addImage(), which does the actual work of adding the QImage as a * resource to the document, pasting it and adding it to the image name list. * * @param imageName the desired image name. If it is already taken, a number will * be appended to it * @param image the actual image to add */ void addImageHelper( const QString &imageName, const QImage &image ); /** * Helper function to get the list of all QTextImageFormats in the document. */ QList embeddedImageFormats() const; /** * Removes embedded image markers, converts non-breaking spaces to normal spaces * and other fixes for strings that came from toPlainText()/toHtml(). */ void fixupTextEditString( QString &text ) const; /** * Does the constructor work */ void init(); /** * Opens a file dialog to let the user choose an image and then pastes that * image to the editor */ void _k_slotAddImage(); + void _k_slotDeleteLine(); + /// The action that triggers _k_slotAddImage() KAction *actionAddImage; + /// The action that triggers _k_slotDeleteLine() + KAction *actionDeleteLine; + /// The parent class TextEdit *q; /// Whether or not adding or pasting images is supported bool imageSupportEnabled; /** * The names of embedded images. * Used to easily obtain the names of the images. * New images are compared to the the list and not added as resource if already present. */ QStringList mImageNames; /** * Although KTextEdit keeps track of the spell checking state, we override * it here, because we have a highlighter which does quote highlighting. * And since disabling spellchecking in KTextEdit simply would turn off our * quote highlighter, we never actually deactivate spell checking in the * base class, but only tell our own email highlighter to not highlight * spelling mistakes. * For this, we use the KTextEditSpellInterface, which is basically a hack * that makes it possible to have our own enabled/disabled state in a binary * compatible way. */ bool spellCheckingEnabled; }; } // namespace using namespace KPIMTextEdit; void TextEditPrivate::fixupTextEditString( QString &text ) const { // Remove line separators. Normal \n chars are still there, so no linebreaks get lost here text.remove( QChar::LineSeparator ); // Get rid of embedded images, see QTextImageFormat documentation: // "Inline images are represented by an object replacement character (0xFFFC in Unicode) " text.remove( 0xFFFC ); // In plaintext mode, each space is non-breaking. text.replace( QChar::Nbsp, QChar::fromAscii( ' ' ) ); } TextEdit::TextEdit( const QString& text, QWidget *parent ) : KRichTextWidget( text, parent ), d( new TextEditPrivate( this ) ) { d->init(); } TextEdit::TextEdit( QWidget *parent ) : KRichTextWidget( parent ), d( new TextEditPrivate( this ) ) { d->init(); } TextEdit::~TextEdit() { } bool TextEdit::eventFilter( QObject*o, QEvent* e ) { if ( o == this ) KCursor::autoHideEventFilter( o, e ); return KRichTextWidget::eventFilter( o, e ); } void TextEditPrivate::init() { q->setSpellInterface( q ); // We tell the KRichTextWidget to enable spell checking, because only then it will // call createHighlighter() which will create our own highlighter which also // does quote highlighting. // However, *our* spellchecking is still disabled. Our own highlighter only // cares about our spellcheck status, it will not highlight missspelled words // if our spellchecking is disabled. // See also KEMailQuotingHighlighter::highlightBlock(). spellCheckingEnabled = false; q->setCheckSpellingEnabledInternal( true ); KCursor::setAutoHideCursor( q, true, true ); q->installEventFilter( q ); } void TextEdit::keyPressEvent ( QKeyEvent * e ) { if ( e->key() == Qt::Key_Return ) { QTextCursor cursor = textCursor(); int oldPos = cursor.position(); int blockPos = cursor.block().position(); //selection all the line. cursor.movePosition( QTextCursor::StartOfBlock ); cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor ); QString lineText = cursor.selectedText(); if ( ( ( oldPos -blockPos ) > 0 ) && ( ( oldPos-blockPos ) < int( lineText.length() ) ) ) { bool isQuotedLine = false; int bot = 0; // bot = begin of text after quote indicators while ( bot < lineText.length() ) { if( ( lineText[bot] == QChar::fromAscii( '>' ) ) || ( lineText[bot] == QChar::fromAscii( '|' ) ) ) { isQuotedLine = true; ++bot; } else if ( lineText[bot].isSpace() ) { ++bot; } else { break; } } KRichTextWidget::keyPressEvent( e ); // duplicate quote indicators of the previous line before the new // line if the line actually contained text (apart from the quote // indicators) and the cursor is behind the quote indicators if ( isQuotedLine && ( bot != lineText.length() ) && ( ( oldPos-blockPos ) >= int( bot ) ) ) { // The cursor position might have changed unpredictably if there was selected // text which got replaced by a new line, so we query it again: cursor.movePosition( QTextCursor::StartOfBlock ); cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor ); QString newLine = cursor.selectedText(); // remove leading white space from the new line and instead // add the quote indicators of the previous line int leadingWhiteSpaceCount = 0; while ( ( leadingWhiteSpaceCount < newLine.length() ) && newLine[leadingWhiteSpaceCount].isSpace() ) { ++leadingWhiteSpaceCount; } newLine = newLine.replace( 0, leadingWhiteSpaceCount, lineText.left( bot ) ); cursor.insertText( newLine ); //cursor.setPosition( cursor.position() + 2); cursor.movePosition( QTextCursor::StartOfBlock ); setTextCursor( cursor ); } } else KRichTextWidget::keyPressEvent( e ); } else { KRichTextWidget::keyPressEvent( e ); } } bool TextEdit::isSpellCheckingEnabled() const { return d->spellCheckingEnabled; } void TextEdit::setSpellCheckingEnabled( bool enable ) { EMailQuoteHighlighter *hlighter = dynamic_cast( highlighter() ); if ( hlighter ) hlighter->toggleSpellHighlighting( enable ); d->spellCheckingEnabled = enable; emit checkSpellingChanged( enable ); } bool TextEdit::shouldBlockBeSpellChecked( const QString& block ) const { return !isLineQuoted( block ); } bool KPIMTextEdit::TextEdit::isLineQuoted( const QString& line ) const { return quoteLength( line ) > 0; } int KPIMTextEdit::TextEdit::quoteLength( const QString& line ) const { bool quoteFound = false; int startOfText = -1; for ( int i = 0; i < line.length(); i++ ) { if ( line[i] == QLatin1Char( '>' ) || line[i] == QLatin1Char( '|' ) ) quoteFound = true; else if ( line[i] != QLatin1Char( ' ' ) ) { startOfText = i; break; } } if ( quoteFound ) { if ( startOfText == -1 ) startOfText = line.length() - 1; return startOfText; } else return 0; } const QString KPIMTextEdit::TextEdit::defaultQuoteSign() const { return QLatin1String( "> " ); } void TextEdit::createHighlighter() { EMailQuoteHighlighter *emailHighLighter = new EMailQuoteHighlighter( this ); setHighlighterColors( emailHighLighter ); //TODO change config KRichTextWidget::setHighlighter( emailHighLighter ); if ( !spellCheckingLanguage().isEmpty() ) setSpellCheckingLanguage( spellCheckingLanguage() ); setSpellCheckingEnabled( isSpellCheckingEnabled() ); } void TextEdit::setHighlighterColors( EMailQuoteHighlighter *highlighter ) { Q_UNUSED( highlighter ); } QString TextEdit::toWrappedPlainText() const { QString temp; QTextDocument* doc = document(); QTextBlock block = doc->begin(); while ( block.isValid() ) { QTextLayout* layout = block.layout(); for ( int i = 0; i < layout->lineCount(); i++ ) { QTextLine line = layout->lineAt( i ); temp += block.text().mid( line.textStart(), line.textLength() ) + QLatin1Char( '\n' ); } block = block.next(); } // Remove the last superfluous newline added above if ( temp.endsWith( QLatin1Char( '\n' ) ) ) temp.chop( 1 ); d->fixupTextEditString( temp ); return temp; } QString TextEdit::toCleanPlainText() const { QString temp = toPlainText(); d->fixupTextEditString( temp ); return temp; } void TextEdit::createActions( KActionCollection *actionCollection ) { KRichTextWidget::createActions( actionCollection ); if ( d->imageSupportEnabled ) { d->actionAddImage = new KAction( KIcon( QLatin1String( "insert-image" ) ), i18n( "Add Image" ), this ); actionCollection->addAction( QLatin1String( "add_image" ), d->actionAddImage ); connect( d->actionAddImage, SIGNAL(triggered(bool) ), SLOT( _k_slotAddImage() ) ); } + + d->actionDeleteLine = new KAction( i18n( "Delete Line" ), this ); + d->actionDeleteLine->setShortcut( QKeySequence( Qt::CTRL + Qt::Key_K ) ); + actionCollection->addAction( QLatin1String( "delete_line" ), d->actionDeleteLine ); + connect( d->actionDeleteLine, SIGNAL(triggered(bool)), SLOT(_k_slotDeleteLine()) ); } void TextEdit::addImage( const KUrl &url ) { QImage image; if ( !image.load( url.path() ) ) { KMessageBox::error( this, i18nc( "@info", "Unable to load image %1.", url.path() ) ); return; } QFileInfo fi( url.path() ); QString imageName = fi.baseName().isEmpty() ? QLatin1String( "image.png" ) : fi.baseName() + QLatin1String( ".png" ); d->addImageHelper( imageName, image ); } void TextEditPrivate::addImageHelper( const QString &imageName, const QImage &image ) { QString imageNameToAdd = imageName; QTextDocument *document = q->document(); // determine the imageNameToAdd int imageNumber = 1; while ( mImageNames.contains( imageNameToAdd ) ) { QVariant qv = document->resource( QTextDocument::ImageResource, QUrl( imageNameToAdd ) ); if ( qv == image ) { // use the same name break; } int firstDot = imageName.indexOf( QLatin1Char( '.' ) ); if ( firstDot == -1 ) imageNameToAdd = imageName + QString::number( imageNumber++ ); else imageNameToAdd = imageName.left( firstDot ) + QString::number( imageNumber++ ) + imageName.mid( firstDot ); } if ( !mImageNames.contains( imageNameToAdd ) ) { document->addResource( QTextDocument::ImageResource, QUrl( imageNameToAdd ), image ); mImageNames << imageNameToAdd; } q->textCursor().insertImage( imageNameToAdd ); q->enableRichTextMode(); } QList< QSharedPointer > TextEdit::embeddedImages() const { QList< QSharedPointer > retImages; QStringList seenImageNames; QList imageFormats = d->embeddedImageFormats(); foreach( const QTextImageFormat &imageFormat, imageFormats ) { if ( !seenImageNames.contains( imageFormat.name() ) ) { QVariant data = document()->resource( QTextDocument::ImageResource, QUrl( imageFormat.name() ) ); QImage image = qvariant_cast( data ); QBuffer buffer; buffer.open( QIODevice::WriteOnly ); image.save( &buffer, "PNG" ); qsrand( QDateTime::currentDateTime().toTime_t() + qHash( imageFormat.name() ) ); QSharedPointer embeddedImage( new EmbeddedImage() ); retImages.append( embeddedImage ); embeddedImage->image = KMime::Codec::codecForName( "base64" )->encode( buffer.buffer() ); embeddedImage->imageName = imageFormat.name(); embeddedImage->contentID = QString( QLatin1String( "%1" ) ).arg( qrand() ); seenImageNames.append( imageFormat.name() ); } } return retImages; } QList TextEditPrivate::embeddedImageFormats() const { QTextDocument *doc = q->document(); QList retList; QTextBlock currentBlock = doc->begin(); while ( currentBlock.isValid() ) { QTextBlock::iterator it; for ( it = currentBlock.begin(); !it.atEnd(); ++it ) { QTextFragment fragment = it.fragment(); if ( fragment.isValid() ) { QTextImageFormat imageFormat = fragment.charFormat().toImageFormat(); if ( imageFormat.isValid() ) { retList.append( imageFormat ); } } } currentBlock = currentBlock.next(); } return retList; } void TextEditPrivate::_k_slotAddImage() { QPointer fdlg = new KFileDialog( QString(), QString(), q ); fdlg->setOperationMode( KFileDialog::Other ); fdlg->setCaption( i18n("Add Image") ); fdlg->okButton()->setGuiItem( KGuiItem( i18n("&Add"), QLatin1String( "document-open" ) ) ); fdlg->setMode( KFile::Files ); if ( fdlg->exec() != KDialog::Accepted ) { delete fdlg; return; } const KUrl::List files = fdlg->selectedUrls(); foreach ( const KUrl& url, files ) { q->addImage( url ); } delete fdlg; } void KPIMTextEdit::TextEdit::enableImageActions() { d->imageSupportEnabled = true; } QByteArray KPIMTextEdit::TextEdit::imageNamesToContentIds( const QByteArray &htmlBody, const KPIMTextEdit::ImageList &imageList ) { QByteArray result = htmlBody; if ( imageList.size() > 0 ) { foreach( const QSharedPointer &image, imageList ) { const QString newImageName = QLatin1String( "cid:" ) + image->contentID; QByteArray quote( "\"" ); result.replace( QByteArray( quote + image->imageName.toLocal8Bit() + quote ), QByteArray( quote + newImageName.toLocal8Bit() + quote ) ); } } return result; } void TextEdit::insertFromMimeData( const QMimeData *source ) { // Add an image if that is on the clipboard if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled ) { QImage image = qvariant_cast( source->imageData() ); QFileInfo fi( source->text() ); QString imageName = fi.baseName().isEmpty() ? i18nc( "Start of the filename for an image", "image" ) : fi.baseName(); d->addImageHelper( imageName, image ); return; } // Attempt to paste HTML contents into the text edit in plain text mode, // prevent this and prevent plain text instead. if ( textMode() == KRichTextEdit::Plain && source->hasHtml() ) { if ( source->hasText() ) { insertPlainText( source->text() ); return; } } KRichTextWidget::insertFromMimeData( source ); } bool KPIMTextEdit::TextEdit::canInsertFromMimeData( const QMimeData *source ) const { if ( source->hasHtml() && textMode() == KRichTextEdit::Rich ) return true; if ( source->hasText() ) return true; if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled ) return true; return KRichTextWidget::canInsertFromMimeData( source ); } static bool isCharFormatFormatted( const QTextCharFormat &format, const QFont &defaultFont, const QTextCharFormat &defaultBlockFormat ) { if ( !format.anchorHref().isEmpty() || format.font() != defaultFont || format.isAnchor() || format.verticalAlignment() != defaultBlockFormat.verticalAlignment() || format.underlineStyle() != defaultBlockFormat.underlineStyle() || format.foreground().color() != defaultBlockFormat.foreground().color() || format.background().color() != defaultBlockFormat.background().color() ) return true; return false; } static bool isBlockFormatFormatted( const QTextBlockFormat &format, const QTextBlockFormat &defaultFormat ) { if ( format.alignment() != defaultFormat.alignment() || format.indent() != defaultFormat.indent() || format.textIndent() != defaultFormat.textIndent() ) return true; return false; } /// @return true if the format represents a list, table, image or something like that. static bool isSpecial( const QTextFormat &charFormat ) { return charFormat.isFrameFormat() || charFormat.isImageFormat() || charFormat.isListFormat() || charFormat.isTableFormat(); } bool TextEdit::isFormattingUsed() const { if ( textMode() == Plain ) return false; // Below, we walk through all text blocks and through all text fragments in them // and check if any of those has any formatting. // To check if they have formatting, we use the functions isBlockFormatFormatted() and // isCharFormatFormatted(). Those do not check all the exising formatting possibilities on // earth, but everything that KRichTextEdit supports at the moment. // // Also, we have to compare the formats against those of a default text edit. For example, // we can't compare the foreground color against black, because the user might have another // color scheme. Therefore we compare the foreground color against a default text edit. QTextEdit defaultTextEdit; QTextCharFormat defaultCharFormat = defaultTextEdit.document()->begin().charFormat(); QTextBlockFormat defaultBlockFormat = defaultTextEdit.document()->begin().blockFormat(); QFont defaultFont = document()->defaultFont(); QTextBlock block = document()->firstBlock(); while ( block.isValid() ) { if ( isBlockFormatFormatted( block.blockFormat(), defaultBlockFormat ) ) { return true; } if ( isSpecial( block.charFormat() ) || isSpecial( block.blockFormat() ) || block.textList() ) { return true; } QTextBlock::iterator it = block.begin(); while ( !it.atEnd() ) { QTextFragment fragment = it.fragment(); QTextCharFormat charFormat = fragment.charFormat(); if ( isSpecial( charFormat ) ) { return true; } if ( isCharFormatFormatted( fragment.charFormat(), defaultFont, defaultCharFormat ) ) { return true; } it++; } block = block.next(); } if ( toHtml().contains( QLatin1String( "
      " ) ) ) return true; return false; } +void TextEditPrivate::_k_slotDeleteLine() +{ + q->deleteCurrentLine(); +} + +void TextEdit::deleteCurrentLine() +{ + QTextCursor cursor = textCursor(); + QTextBlock block = cursor.block(); + const QTextLayout* layout = block.layout(); + + // The current text block can have several lines due to word wrapping. + // Search the line the cursor is in, and then delete it. + for ( int lineNumber = 0; lineNumber < layout->lineCount(); lineNumber++ ) { + QTextLine line = layout->lineAt( lineNumber ); + const bool lastLineInBlock = ( line.textStart() + line.textLength() == block.length() - 1 ); + const bool oneLineBlock = ( layout->lineCount() == 1 ); + const int startOfLine = block.position() + line.textStart(); + int endOfLine = block.position() + line.textStart() + line.textLength(); + if ( !lastLineInBlock ) + endOfLine -= 1; + + // Found the line where the cursor is in + if ( cursor.position() >= startOfLine && cursor.position() <= endOfLine ) { + int deleteStart = startOfLine; + int deleteLength = line.textLength(); + if ( oneLineBlock ) + deleteLength++; // The trailing newline + + // When deleting the last line in the document, + // remove the newline of the line before the last line instead + if ( deleteStart + deleteLength >= document()->characterCount() && + deleteStart > 0 ) + deleteStart--; + + cursor.beginEditBlock(); + cursor.setPosition( deleteStart ); + cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor, deleteLength ); + cursor.removeSelectedText(); + cursor.endEditBlock(); + return; + } + } + +} + + #include "textedit.moc" diff --git a/kpimtextedit/textedit.h b/kpimtextedit/textedit.h index 5fccddc34..5358e689a 100644 --- a/kpimtextedit/textedit.h +++ b/kpimtextedit/textedit.h @@ -1,263 +1,271 @@ /* Copyright (c) 2009 Thomas McGuire Based on KMail and libkdepim code by: Copyright 2007 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KPIMTEXTEDIT_TEXTEDIT_H #define KPIMTEXTEDIT_TEXTEDIT_H #include "kpimtextedit_export.h" #include #include #include #include class KUrl; namespace KPIMTextEdit { class TextEditPrivate; class EMailQuoteHighlighter; /** * Holds information about an embedded HTML image. * A list with all images can be retrieved with TextEdit::embeddedImages(). */ struct EmbeddedImage { QByteArray image; ///< The image, encoded as PNG with base64 encoding QString contentID; ///< The content id of the embedded image QString imageName; ///< Name of the image as it is available as a resource in the editor }; typedef QList< QSharedPointer > ImageList; /** * Special textedit that provides additional features which are useful for PIM applications * like mail clients. * Additional features this class provides: * - Highlighting quoted text * - Handling of inline images * - Auto-Hiding the cursor * - Handling of pastes and drops of images * * @since 4.3 */ class KPIMTEXTEDIT_EXPORT TextEdit : public KRichTextWidget, protected KTextEditSpellInterface // TODO: KDE5: get rid of the spell interface { Q_OBJECT public: /** * Constructs a TextEdit object * @param text the initial plain text of the text edit, interpreted as HTML * @param parent the parent widget */ explicit TextEdit( const QString& text, QWidget *parent = 0 ); /** * Constructs a TextEdit object. * @param parent the parent widget */ explicit TextEdit( QWidget *parent = 0 ); /** * Calling this allows createActions() to create the add image actions. * Call this method before callilng createActions(), otherwise the action * will not be added. * Also, if image actions is enabled, the user can paste PNG images. * * Don't call this if you don't want to support adding images. */ void enableImageActions(); /** * Destructor */ ~TextEdit(); /** * Reimplemented from KMEditor, to support more actions. * * The additional action XML names are: * - add_image + * - delete_line * * The add_image actions is only added if enableImageActions() is called before. */ virtual void createActions( KActionCollection *actionCollection ); /** * Adds an image. The image is loaded from file and then pasted to the current * cursor position. * * @param url The URL of the file which contains the image */ void addImage( const KUrl &url ); + /** + * Deletes the line at the current cursor position. + * @since 4.4 + */ + void deleteCurrentLine(); + /** * Get a list with all embedded HTML images. * If the same image is contained twice or more in the editor, it will have only * one entry in this list. * * @return a list of embedded HTML images of the editor. */ ImageList embeddedImages() const; /** * Returns the text of the editor as plain text, with linebreaks inserted * where word-wrapping occurred. */ QString toWrappedPlainText() const; /** * Same as toPlainText() from QTextEdit, only that it removes embedded images and * converts non-breaking space characters to normal spaces. */ QString toCleanPlainText() const; /** * This method is called after the highlighter is created. * If you use custom colors for highlighting, override this method and set the colors * to the highlighter in it. * * The default implementation does nothing, therefore the default colors of the * EMailQuoteHighlighter class will be used. * * @param highlighter the highlighter that was just created. You need to set the colors * of this highlighter. */ virtual void setHighlighterColors( EMailQuoteHighlighter *highlighter ); /** * Convenience method for qouteLength( line ) > 0 */ bool isLineQuoted( const QString &line ) const; /** * This is called whenever the editor needs to find out the length of the quote, * i.e. the length of the quote prefix before the real text starts. * The default implementation counts the number of spaces, '>' and '|' chars in * front of the line. * * @param line the line of which the length of the quote prefix should be returned * @return 0 if the line is not quoted, the length of the quote prefix otherwise * FIXME: Not yet used in all places, e.g. keypressEvent() or the quote highlighter */ virtual int quoteLength( const QString &line ) const; /** * Returns the prefix that is added to a line that is quoted. * By default, this is "> ". */ virtual const QString defaultQuoteSign() const; /** * For all given embedded images, this function replace the image name in the tag of the * HTML body with cid:content-id, * so that the HTML references the image body parts, see RFC 2557. * * This is useful when building a MIME message with inline images. * * Note that this function works on encoded content already. * * @param htmlBody the HTML code in which the tag will be modified. * The HTML code here could come from toHtml(), for example. * * @param imageList the list of images of which the tag will be modified. * You can get such a list from the embeddedImages() function. * * @return a modified HTML code, where the tags got replaced */ static QByteArray imageNamesToContentIds( const QByteArray &htmlBody, const ImageList &imageList ); /** * Checks if rich text formatting is used anywhere. * This is not the same as checking whether textMode() returns "Rich", since * that only tells that rich text mode is enabled, but not if any special formatting * is actually used. * * @return true if formatting is used anywhere */ bool isFormattingUsed() const; protected: /** * Reimplemented for inline image support */ virtual bool canInsertFromMimeData( const QMimeData *source ) const; /** * Reimplemented for inline image support */ virtual void insertFromMimeData( const QMimeData *source ); /** * Reimplemented from KRichTextWidget to hide the mouse cursor when there * was no mouse movement for some time, using KCursor */ virtual bool eventFilter( QObject*o, QEvent* e ); /** * Reimplemented to add qoute signs when the user presses enter * on a quoted line. */ virtual void keyPressEvent ( QKeyEvent * e ); // For the explaination for these four methods, see the comment at the // spellCheckingEnabled variable of the private class. /** * Reimplemented from KTextEditSpellInterface */ virtual bool isSpellCheckingEnabled() const; /** * Reimplemented from KTextEditSpellInterface */ virtual void setSpellCheckingEnabled( bool enable ); /** * Reimplemented from KTextEditSpellInterface, to avoid spellchecking * quoted text. */ virtual bool shouldBlockBeSpellChecked( const QString& block ) const; /** * Reimplemented to create our own highlighter which does quote and * spellcheck highlighting */ virtual void createHighlighter(); private: std::auto_ptr const d; friend class TextEditPrivate; Q_PRIVATE_SLOT( d, void _k_slotAddImage() ) + Q_PRIVATE_SLOT( d, void _k_slotDeleteLine() ) }; } // namespace #endif